第一部分:自動內存管理
總圖:
部分名詞解釋:
slot(槽):指棧中存放局部變量的容器。double、long占2個slot,其他占1個slot。注意:并不用指定一個槽多大。因為局部變量的大小都是確定的。
TLAB:Thread Local Allocation Buffer,線程私有的分配緩沖區。
-
直接內存:Direct Memory,其類似于真實物理內存,是不屬于上述運行時數據區的內存分區。保存了對象的實現,而對象的引用可以放在堆內,如此可提高性能。
需要注意配置java堆時(有的jvm必須有最大限制,且有默認值),要考慮上直接內存,否則也會Out Of Memory
OOP:Object Oriented Programming,面向對象編程
Oop: Ordinary Object Pointer,普通對象指針
JMC(Java Mission Control):java任務控制
JFR(Java Flight Recorder):
全限定名:名字里包含了目錄,用作接口、類。相對應的是簡單名,只是方法或字段的名字。
參數:
-verbose:gc ——打開gc的跟蹤日志
-XX:+printGC——打開GC的log的開關,簡要日志
-XX:+PrintGCDetails:打印GC的詳細信息
-XX:+TraceClassLoading(監控類加載,可以在程序運行時檢出哪些類被加載了
-XX:+PrintClassHistogram(加入此參數,在運行時不會有其他東西輸出,但是在按下Ctrl+Break后可以打印出類的信息,類的直方圖)
-Xmx(最大堆的空間)
-Xms(最小堆的空間)
-Xmn (設置新生代的大小)
-XX:NewRatio(設置新生代和老年代的比值,如果設置為4則表示(eden+from(或者叫s0)+to(或者叫s1)): 老年代 =1:4),即年輕代占堆的五分之一
-XX:SurvivorRatio(設置兩個Survivor(幸存區from和to或者叫s0或者s1區)和eden區的比),8表示兩個Survivor:eden=2:8,即Survivor區占年輕代的五分之一
-XX:+HeapDumpOnOutOfMemoryError(將OOM時的堆信息導出到文件)
如果系統出現OOM一般情況系統有可能會down掉,但是我們排查問題時需要場景重現是比較困難的,所以當我們輸出了OOM的異常時,就可以直接查看,找出導致OOM的原因-XX:+HeapDumpPath=XXXX(導出OOM堆信息文件的路徑)
-XX:OnOutOfMemoryError(在系統出現OOM時,執行一個腳本,可以發送郵件,報警或者是重啟程序)
-XX:PermSize(設置永久代的初始空間大小)
-XX:MaxParmSize(設置永久代的最大空間)
-Xss(設置棧空間的大小)
可能問題及原因:
-
StackOverFlowError:
當線程調用的棧深度超過jvm所允許也會報。(虛擬機內存容量不夠)
或者棧幀太大(一個方法內部的變量太多)時,無法申請足夠的空間也會報。
-
OutOfMemory:
某些jvm棧容量是可以動態擴展的。拓展無法申請到足夠內存時,會報出此錯誤。(HotSpot虛擬機不允許棧動態擴展,所以不會出現這種原因的報錯,但申請失敗時也會報這個錯誤。)
對于堆來說,一般都是可拓展的,當有線程申請分配,但其內部空間不夠,而且堆也無法拓展時,會報錯。
對于方法區,當其無法提供新的內存分配需求時,會報錯。如運行時添加的常量,也會向方法區申請內存,但如果方法區內存已滿,且無法拓展時,變會報內存溢出。
java堆
部分名詞解釋:
- 指針碰撞:Bump The Pointer,描述java堆分配新內存時的動作。
1.對象的創建
結構:
對象頭(header):大小為一個Mark Word,32or64位(同于系統)。包含:對象哈希嗎、對象分帶年齡、存儲鎖標志位。(還可能有:類型指針,指向類的元數據(java方法區的常量池中);java數組header會有一個總大小header字段、以此判斷該數組對象大小)
// Bit-format of an object header (most significant first, big endian layout belo// // 32 bits: // -------- // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) // size:32 ------------------------------------------>| (CMS free block) // PromotedObject*:29 ---------->| promobits:3 ----->| (CMS promoted object)
-
實例數據(Instance Data):包含從父類繼承的和本類的數據。排序方式默認為從大到小,即:long\double、int、short...還有一些其他設定,如compactFields,緊湊字段設置,可以將小數據插入其他數據中間。
所以順序和代碼里面的順序可能不同。
對象填充(Padding):hospot要求每個對象都是8字節對齊。所以在實例數據尾部可能會有填充。(對象頭已是8字節對齊)
java棧
StackOverflowError內存泄漏原因:(如下方法是hotspot jvm中,其不可拓展棧幀長度,并設置了棧幀最大值)
①棧深度過深:線程(方法)的棧深度超過jvm限制許可,如我測試時1050左右便不允許再深入。
②棧幀太大/jvm容量不夠:一個方法內的變量太多,導致雖然沒有達到棧深度限制,但是無法申請足夠的內存。
說明:堆棧溢出,說明可能是其本身的問題,并非物理內存不夠。和OutOfMemory,內存溢出不相同。
OutOfMemory內存溢出原因:(尤其是在32位系統應用開發時,更應該注意。)
①線程過多:線程創建過多,也類似棧溢出中②的問題,并可能導致操作系統假死。
②jvm容量不夠/物理內存也不夠:當棧可以拓展時,可能不會出現棧溢出,而是會出現內存溢出的問題。java.lang.OutOfMemoryError: unable to create native thread
方法區(Methods Area)
說明:主要職責在于存放class的信息:如類名、訪問修飾符、常量池、字段描述、方法描述等
OutOfMemory內存溢出原因:
①(jdk6及以前)常量池容量不夠:可以通過限制常量池大小 -XX:PermSize=6M -XX:MaxPermSize=6M等來限制,并一直向常量池添加數據(可以通過String::intern()來進行),會出現這個報錯。但在jdk7以后,永久代漸漸取消、jdk8之后永久代完全放棄,不能得到常量池溢出(會出現堆溢出),因為放在永久代的字符串常量池從方法區轉移到了堆中。
CGLib開源項目:http://cglib.sourceforge.net/。
垃圾回收(Garbage Collection)
圖示:hotspot的分代垃圾收集器。線相連代表可以一起配合使用。(沒有最好,只有適合)下面的組合上有jdk9的,代表已取消支持搭配。(CMS,Concurrent Mark Sweep,也被稱為并發低停頓收集器,并行標記掃描;G1,Garbage First,其可處理整個堆中的區域,是收集器技術發展的里程碑;)
parallel 并行:指多條垃圾收集器線程之間的關系,同時有多條線程在協同工作; 主要用于服務器后臺,提升吞吐量(運行用戶代碼時間 /(運行用戶代碼時間+運行收集器線程時間))。Parallel Scavenge還有一個名稱“吞吐量優先收集器”(配合Parallel Old)
Concurrent 并發:指垃圾收集器線程和用戶線程之間的關系,說明垃圾收集器線程和用戶線程同時運行。如CMS等收集器主要用于客戶端,減少用戶線程停頓時間,提升服務質量和交互能力。
Shenandoah:是其他公司開發的一個收集器,其目標是low-pause低延遲,并且確實做得好。其也是全堆通用,因為其并沒有進行分代,而是想G1將內存劃分成多個region。
三大指標:內存占用(footprint)、吞吐量(時間比)、延遲(今后最被重視的指標)
other:https://blogs.oracle.com/jonthecollector/our_collectors。牽掛你的人
Parallel Scavenge:目標是為了獲得最大吞吐量(上述定義),多用于服務器使用。
CMS:目標是為了獲得最短暫停時間,多用于客戶端或互聯網服務器,為了提供短暫的系統停頓時間,提高客戶服務。
說明:主要工作解決的問題是:①如何定位應被刪除對象;②何時刪除對象;③如何刪除對象。
GC Roots:
·在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的
參數、局部變量、臨時變量等。
·在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
·在方法區中常量引用的對象,譬如字符串常量池(String Table)里的引用。·在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
·Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如
NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
·所有被同步鎖(synchronized關鍵字)持有的對象。
·反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
垃圾回收算法:Richard Jones撰寫的《垃圾回收算法手冊》
垃圾收集算法可以劃分為“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類
分代收集(generational Collection):
說明:其理論建立在兩個假說上面:強分代假說(Strong Generational Hypothesis)、弱分代假說(Weak Generational Hypothesis)。
逃過一次GC,年齡就加一,逃過越多次GC的對象,就越難以消除。而絕大多數對象都是朝生夕死的,以此將堆空間分出兩個區,強分代的GC頻率低、弱分代GC頻率高。
以此中和內存空間利用率和GC算法性能(時間)消耗。由分代收集劃分出不同內存區域的思想,其后延伸出很多算法和劃分方式。
內存劃分如:新生代(young generation)、老年代(tenured generation)等,并因其可能會互相引用、而延伸出第三個假說:跨代引用假說(Intergerenational preference hypothesis)。
跨帶引用假說:由于將所有老年代作為GC Roots比較耗性能,故在新生代內存區域建立一個記憶集(remembered set)全局數據結構,將老年代分為小塊:有對新生代的引用,和沒有的。minor GC掃描時只需要將有引用那塊的老年代作為GC Roots即可。
GC劃分如:Partial GC:{ Minor GC/ Young GC、Major GC / Old GC、Mixed GC } 、 Full GC;
如:minor GC只會對新生代進行掃描;Major GC只針對老年代,但一般的jvm很少會單獨手機老年代,只有G1這樣做,并且Major在不同jvm中,定義可能不同。
收集器清除算法:
基礎算法是1960 Lisp之父提出的Mark-Sweep 標記-清除算法。其后很多算法都是以其為基礎,進行改進得到。
標記-清除算法:全部進行標記(可以標記存活或回收的),然后一次清除。
標記-復制算法:通過將內存區域劃分為兩個區:使用區和空閑區。每次垃圾清除,將存活的對象復制到空閑區,然后清除使用區,并交換兩者區域屬性。1989年Andrew Appel針對具備“朝生夕滅”特點的對象,提出了一種更優化的半區復制分代策略,現在稱為“Appel式回收”。
Appel式回收:將新生代分為1個eden(伊甸園)和2個survivor,一次保留一個survivor不使用。如hotspot中eden和survivor比例為8:1,所以一次使用(8+1)/10=90%的內存區域,不算太浪費。如果survivor不夠時,會分配到老年代中,并在eden清空后分配回來。標記-復制算法和appel,其理論依據都是在于新生代對象存活率很低的情況下, 而這一般復合現實的規律。但老年代的存活率很高,其復制開銷也會很大,不能用這種算法。
標記-整理算法(Mark-Compact):(compact,緊湊的,v壓縮)多用于老年代,和標記-清除算法很像,也是先標記,然后清除,但是其清除后,會對老年代存活對象進行移動,使之緊湊,解決了內存碎片化問題,但由于要移動和更改引用,其間會暫停線程蠻長時間(標記-清除也會,但很短),延遲還是蠻高的。(hotspot 的Parallel Scavenge收集器)
混合算法:對于老年代,可以先采取標記-清除算法,直到內存碎片已經影響內存分配,進行一次標記-清理算法,hotspot的cms算法就是這樣做的。
并發:當收集器和其他線程并發時,要避免出現”對象消失“問題:以三色來理解:{ 黑:代表其已被掃描,且所引用的對象都已被掃描。灰:代表自身已被掃描,但其所引用對象還沒有(執行ing);白:代表沒有被掃描 ;見https://en.wikipedia.org/wiki/Tracing_garbage_collection#Tri-color_marking。}
當并行時,可能出現灰色對象取消白色對象的引用,但之前的黑色對象卻又引用了白色對象,此時白色對象會被錯誤編入待清除對象中。我們要解決這個問題。
“對象消失”問題出現的兩個必要:
賦值器插入了一條或多條從黑色對象到白色對象的新引用; (解決方案:增量更新;掃描完后再掃描中途有引用其他對象行為的黑色節點。cms收集器)
賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。(解決方案: 原始快照;中途有刪除引用關系的,不直接刪除,而是以開始時的快照進行,結束后再進行掃描。G1、Shenandoah收集器)
我們只要中斷一個就可以。
計數收集:
java工具
...java/bin/里面,有很多命令行工具,具體看深入理解java虛擬機 4.2.6,
還有http://openjdk.java.net/jeps/320。
還有一些可視化工具、虛擬機插件及工作。(JHSDB、JConsole、Visual VM、BTrace(visual VM插件,也可以獨立)、JFR、JMC、HSDIS用于輸出匯編代碼和JITWacth搭配)
class文件
用hex來查看class文件,用命令行工具javap來翻譯class文件:javap -verbose filePath。
- 部分名詞解釋:
- 類型后綴_info:類型分為無符號類型和表。無符號用u1、u2、u4、u8表示,代碼字節個數;表用info表示,里面可能有多個無符號和表數據類型;
格式:固定順序的:
①(u4-四字節)magic number:標明這個文件的格式。如class文件是:xCAFEBABE
②(u4)minor version(次版本號=2bytes)和majoy version(主版本號=2bytes):主版本號從45開始,次版本號為0~65535.如jdk1 majoy version =45、jdk13 majoy version=57。
③(u2-兩字節)constant pool count(常量池里面的數量)。
④(中間省略所有常量池的常量....)常量池里面的數據共有十七中結構(到jdk12)幾乎都不同,但第一項相同,是標簽tag,如下表所示,共有17種。具體常量池的結構看:常量池內容
⑤(u2)Access tag(訪問標志):共有16個標志位可用,但當前只有9個被定義了(每個標志位用二進制0/1來標志)。主要用作對方法的標識,如其是否是普通類還是abstract、接口類,or其是否是public等。
⑥(u23)this_class* (類索引)、super_class(父類索引)、interfaces(接口索引):每個2字節,索引的目的地為常量池對象列表,如0x0001,指常量池中第一個對象,也就是上述常量池中第一個常量數據。
⑦( u2)field count:字段表集合數量。
⑧(每字段4對u2+可拓展的屬性表):標識該類里面的字段表數據,可能有多個字段。每一個字段表數據有如下結構:
access_flags(u2、類似于上述訪問標志,標識該字段的修飾符等類型,如是否是public等)+
name_index(u2、常量池索引) +
descriptor_index(u2、常量池索引):{ 結構:參數列表"(...)" + 返回值:int fun(int x, char []b) = (I[C)I };
attribute_count(u2):表示屬性表的數量。
后面還有可能附加attribute_info表,里面是附加信息。如:int x=30;給定的初始值會作為常量池中的常量,而附加屬性對其引用。
⑨(u2)方法區數量。
⑩(4對u2+屬性表集合)方法區:屬性表集合擁有很多屬性_info,具體看下文
常量池內容
方法表
和字段表很類似。其屬性表中會有code
結構為:4個u2+方法的屬性表:(順序)access_flag、name_index、descriptor_index(說明參數和返回值)、屬性表個數、n個具體屬性表(如Code表,下面有詳述)
屬性表
屬性表通用格式:
屬性表集合:
Code屬性表:
max_stack:最大棧深度,虛擬機運行時根據這個值來分配棧幀中操作棧深度。
max_locals:最大局部變量的值,定義槽slot的數量,一個槽可以放32位,但只能放一個變量,64位的要兩個槽。其所定義的槽數量,并非所有局部變量的數量和,而是同一時間,存活的最大局部變量數量和類型計算出max_locals的值。以節約內存。
code_length:字節碼的長度,雖然是u4,但實際上超過65535(u2)字節碼就會拒絕編譯。如jsp內面和內容,可能會規定到一個方法中,可能會超長編譯失敗。
code:由length個u1組成,每條指令的第一個字節u1,類似于處理器指令集,指出該指令的意義和該指令的長度等。《java虛擬機規范》定義了越200條編碼值對應的指令意義。詳情附錄C“虛擬機字節碼指令表”。
exception_table_length:異常表長度
exception_table:異常表代碼,也是用了指令集
Exception屬性表
說明:與上述Code屬性表中的異常表不同,這個異常屬性表,列出的是方法拋出的異常。
格式:
number_of_exceptions :表示異常種類的個數。
exception_index_table:索引常量池中的Constant_Class_info型常量,代表該被檢查的異常的類型的名字。
LineNumberTable屬性表
功能:主要用于將java源碼行號與code字節碼偏移量進行關聯映射。方便調試操作等,但非必須。javac中編譯時,可以輸入-g:none、-g:lines來取消關聯。
line_number_table:包含多個line_number_info類型的數據。
line_number_info表:包含start_pc和line_number兩個u2類型的數據項,前者是字節碼行號,后者是Java源
碼行號。
LocalVariableTable及LocalVariableTypeTable屬性表
功能:將java源碼局部變量與棧幀中局部變量聯系起來。當別人引用這個方法時,參數名也會存在,如果取消此關聯,可能外部引用時,局部變量名字會丟失,用args0、args1代替,雖然不影響運行,但不方便調試;編譯時可用javac -g:none或 -g:vars來關閉。
格式:
name_index和description_index:都是對常量池進行索引,得到變量名,參數和返回類型。
index:是變量槽中的偏移量。如果是64位,其對應的值時index和index+1。
LocalVariableTypeTable:這個新增的屬性結構與LocalVariableTable非常相似,僅僅是把記錄的字段描述
符的descriptor_index替換成了字段的特征簽名(Signature)。對于非泛型類型來說,描述符和特征簽名
能描述的信息是能吻合一致的,但是泛型引入之后,由于描述符中泛型的參數化類型被擦除掉[3],描
述符就不能準確描述泛型類型了。因此出現了LocalVariableTypeTable屬性,使用字段的特征簽名來完
成泛型的描述。
SourceFile、SourceDebugExtension屬性表
功能:生成.class文件的源文件名,一般類名和源文件名都是一樣的。
關閉:-g: none \ -g:source
格式:(u2)屬性名索引、(u4)屬性長度、(u2)常量池索引(得到.java源文件名)
SoureceDebugExtension:為了方便在編譯器和動態生成的Class中加入供程序員使用的自定義內容,在JDK 5時,新增了 SourceDebugExtension屬性用于存儲額外的代碼調試信息。典型的場景是在進行JSP文件調試時,無法通過Java堆棧來定位到JSP文件的行號。JSR 45提案為這些非Java語言編寫,卻需要編譯成字節碼并運行在Java虛擬機中的程序提供了一個進行調試的標準機制,使用SourceDebugExtension屬性就可以用于存儲這個標準所新加入的調試信息,譬如讓程序員能夠快速從異常堆棧中定位出原始JSP中出現問題的行號。
格式:
ConstantValue 屬性表
innerClasses 屬性表
說明:一個類包含了內部類,則會生成該屬性表,用于記錄內部類和宿主類之間的關聯。
[圖片上傳失敗...(image-782504-1594430035540)]
inner_class_info_index和outer_class_info_index:分別代表內部類和宿主類在常量池中的類型為CONSTANT_Class_info符號引用。
inner_name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表這個內部類的名稱,如果是匿名內部類,這項值為0。
inner_class_access_flags:內部類的訪問標志,類似于類的access_flags,它的取值范圍如表6-26所示。
Deprecated、Synthetic屬性表
Deprecated:(棄用的)通過在方法、字段前添加@Deprecated來設置。編譯時,會在屬性表中進行簡單描述。
SYnthetic:(人造的)標識該方法、字段、類是編譯器自動生成的,非從源代碼中來。
格式:
(u2)屬性名常量池索引 + (u4)屬性長度(恒定為0x00000000)。
StackMapTable
Signature屬性表
BootStrapMethods屬性表
MethodParameters屬性表
還有兩個屬性沒有寫
字節碼指令簡介
說明:java虛擬機采用的是面向操作數棧而非寄存器的架構。
詳情:Java虛擬機規范(Java SE 7)——第六章。
名詞解釋:
Opcode:操作碼。1字節大小;用于標識特定操作。其后跟隨0~多個該操作需要的參數。
Operand:操作數。跟在操作碼后面的數據。
助記符:操作碼的助記符,用來簡述操作的意義。其中特殊字符用作表明服務的數據類型。l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。也有的沒有特殊字符。
說明:限于一字節大小的指令集,對于類型大小<int的,如byte、char、short等,編譯時都會進行拓展成int來操作。包括數組如char數組都會拓展成int數組來操作。
加載和存儲指令
功能:用于將數據,在棧幀中的{局部變量表}、{操作數棧}之間相互傳輸。
從局部變量加載到操作棧:iload、iload_<n>、fload、 fload _<n>、dload、dload _<n> 、aload、aload _<n>
從操作棧存儲到局部變量:istore、istore_<n>等
-
將常量加載到操作數棧:bipush、sipusldc、ldc_w、ldc2_w、aconst_null、iconst_m1、
iconst_ <i>、lconst _<l>、fconst _<f>、dconst _<d> 。
拓展局部變量表的訪問索引:wide
<n>:上述如iload_<n>指令中的 _<n>代表了一組指令。算是iload的特殊形式。
運算指令
說明:對byte、short、char、boolean類型的算術指令,用int類型的指令來代替。《java虛擬機規范》指出, 只有xdiv 、xrem 中出現余數為0時,會拋出ArithmeticException異常。
算術指令:用x代替(i、l、f、d);iadd、ladd、fadd、dadd;xsub;xmul;xdiv;
xrem(取反);xshl、xshr(位移);ior、lor(按位或);iand、land(按位與);ixor、lxor(按位異或);iinc(自增);·比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp;
類型轉換指令
寬化類型轉換(Widening Numeric Conversion):從小范圍類型轉化為大范圍。如int -> long/float/double;隱式即可轉換。
窄化類型轉換(Widening Numeric Conversion):從大范圍類型轉化為小范圍。如float/double -> long;必須顯示轉換;
窄化類型轉換指令:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
對象創建與訪問指令
創建類的指令:new
創建數組的指令:newarray、anewarray、multianewarray
訪問類字段(static字段,也稱類變量)指令:getstatic、putstatic
訪問實例字段(非static字段,或稱實例的變量):getfield、putfield
將數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
操作數棧的值儲存到數組元素的指令:bastore’、castore、sastore、iastore、fastore、lastore、fastore、aastore
取數組長度的指令:arraylength
檢查對象實例所屬類型的指令:instanceof、checkcast
操作數棧管理指令
操作數棧 棧頂出棧:pop、pop2(頂上兩個元素出棧)
復制棧頂一個、二個數值,并將復制值壓入棧頂:dup、dup2;dup_x1、dup2_x1;dup2_x2、dup2_x2;
將棧中最頂端的兩個數值互換:swap
控制轉移指令
說明:有條件或無條件地跳轉。如之前所說,byte等數據類型會轉換成int,而long、float、double則會先進行計算xcmpl、xcmpg,返回一個整數值到操作數棧中,隨后在執行int的條件分支比較操作。
條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpgt、if_icmple、if_icompge、if_acmpeq、if_acmpne
復合條件分支:tableswitch、lookupswitch
無條件分支:goto、goto_w、jsr、jsr_w、ret
方法調用和返回指令
invokevirtual:用于調用對象(實例)的方法。
invokeinterface:用于調用接口的方法。
invokespecial:調用一些需特殊處理的方法。包括:實例初始化方法、私有方法、父類方法等。
invokestatic:調用類靜態方法。
invokedynamic:用于在運行時動態解析出調用點限定符所引用的方法。
說明:調用函數與返回值無關,但是返回指令是根據類型進行區分。
ireturn:返回的類型包括int、short、char、byte、boolean。
lreturn、freturn、dreturn、areturn、return(void返回類型)
異常處理指令
athrow:顯式拋出異常。
java虛擬機對異常的處理,不是由字節碼指令來實現。而是由異常表來完成。
同步指令
說明:用monitor(管程,或稱鎖)來實現方法級同步和方法內(一段指令序列)同步。‘
同步:方法執行前持有monitor、然后調用方法,方法結束后釋放monitor。執行期間,其他線程無法在獲得同一個monitor管程。當發生異常時,方法內部無法處理并拋出異常到同步方法邊界外后,會釋放管程。
方法級同步:由虛擬機隱式來完成。虛擬機通過訪問該方法常量池里面的ACC_SYNCHRONIZED訪問標志是否被設置,來決定是否讓該執行線程持有管程,隨后再執行方法。
方法內同步:當java中,方法內部有synchronized語句,則會進行同步。
monitorenter:調用前會執行該指令
-
monitorexit:調用結束前會執行該指令。
在這兩個指令必須配對使用。其中間的指令序列,即是被同步的。如果沒有異常處理程序,虛擬機會自動生成可處理所有異常的異常處理代碼,為的是monitorenter能正確配對。
共有設計和私有實現
如class文件格式和字節碼指令。兩者與硬件、操作系統、具體java虛擬機實現之間是完全獨立的。虛擬機實現者可以充分優化和拓展,已獲得更好的性能。
實現方式主要有兩種:
- 將輸入的java虛擬機代碼在加載時或執行時,翻譯成另外一種虛擬機的指令集。
- 將輸入的java虛擬機代碼在加載時或執行時,翻譯成宿主機處理程序的本地指令集。(即時編譯器代碼生成技術)
虛擬機類加載機制
類的生命周期:
強制初始化
《java虛擬機規范》中對初始化之前的階段是沒有強制要求的,有且僅有以下六種情況,則必須進行初始化:
遇到new、getstatic、putstatic、invokestaitc
使用java.lang.reflect(反射)包的方法對類型進行反射調用時。
當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化(接口不需要初始化其父類)
當虛擬機啟動時,會根據用戶指定的主類(包含main()方法),虛擬機會先初始化這個主類。
-
當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解
析結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句
柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化。
-
當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,如果有
這個接口的實現類發生了初始化,那該接口要在其之前被初始化。
類加載階段
①加載
三件事情:
- 通過一個類的全限定名,來獲取定義此類的二進制字節流。(?如何獲得?)
- 將這個字節流所代表的靜態存儲結構,轉化為方法區的運行時數據結構。
- 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據訪問入口。
②驗證
《java虛擬機規范》中描述比較籠統,不是太具體。大概會完成下面四個階段:
- 文件格式驗證: 驗證字節流是否符合Class文件格式規范。如檢測是否以魔數0xCAFEBABE開頭、主次版本號是否在可運行、常量池的常量中是否有不被支持的常量類型等...內容非常龐多。主要保證輸入的字節流能正確解析,并存儲在虛擬機內存方法區中。(基于二進制流,之后三個階段都基于方法區存儲結構)
- 元數據驗證:主要對類的元數據信息進行語義校驗(對類本身進行校驗),如:是否有父類(除了java.lang.Object之外都應有)、父類是否不允許被繼承、是否實現了其父類或接口之中要求實現的所有方法等等...
- 字節碼驗證:(對類里方法Code屬性進行校驗)最復雜的一個驗證階段,保證被校驗的方法運行時不會危害虛擬機安全。
- 符號引用驗證:校驗類所引用的外部類、方法、字段等。
當確保無誤時,可通過-Xverify:none來關閉,節約虛擬機驗證時間。
③準備
為類中定義的static變量分配內存,并設置變量初始值(0,而非代碼中的初始值,如static int x=120,是要到創建實例才被類構造器< clinit>()初始化;注意:常量除外,常量是直接就初始化為指定值)
④解析
虛擬機將常量池內的符號引用,改成直接引用。
- 符號引用:一組字符,描述引用的目標。目標并不一定已加載到虛擬機內存中。
- 直接引用:直接指向目標的指針、相對偏移量、句柄等。目標必定已在虛擬機中了。
1.類或接口的解析:
非數組(普通類或接口):虛擬機把該類型的符號引用(全限定名)提供給類加載器,類加載器加載這個類到內存中。
數組:和非數組差不多,不過其描述符的形式為:"[Ljava/lang/Integer"。
符號引用驗證:驗證是否本類對引用類有訪問權限,如果無,會拋出java.lang.IllegalAccessError異常。(由于JDK9引入了模塊化概念,Public類型也并非一定可以訪問,要看本模塊是否有引用類所屬模塊的訪問權限。
2.字段解析:
用于解析類中的字段,將字段與實際的所屬類聯系起來,所以會先解析其父類或實現的接口。
- 字段所屬類本身包含該字段的簡單名稱和字段描述符,則返回這個字段的直接引用。
- 否則,先搜索其實現的接口,按繼承關系從下往上搜索所有父接口,直到找到匹配的簡單名稱和字段描述符。
- 否則,在搜索其父類,按繼承關系往上,直到找到匹配的簡單名稱和字段描述符。
- 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。
注意:Oracle公司的javac編譯器在實現時,更加嚴格。當該類本身及父類、所實現接口中,都有該字段的簡單名稱和字段描述符時,會報錯。Javac編譯器將提示“The field Sub.A is ambiguous”,并且會拒絕編譯這段代碼。
3.方法解析:
- 解析方法所屬類,并加載類,解析成功時繼續后續
- 若在類的方法表中進行方法解析,但是卻指向了接口類型的常量池索引,會報錯Java.lang.IncompatibleClassChangeError,若無錯,則繼續
- 此后便是檢查該方法是在本類、還是父類、所實現的接口類中。如果沒有找到拋出java.lang.NoSuchMethodError。
- 查找并成功返回來直接引用后,會對這個方法進行權限驗證,如果不具備訪問該方法權限,則拋出java.lang.IllegalAccessError
4.接口方法解析:
- 解析接口方法所屬接口的符號引用,并解析
- 如上,如果在接口方法表中發現方法引用的常量類型是類,則報不匹配類型改變錯誤。正常則繼續
- 沿本接口、父接口一直往上,直到找到匹配的方法。如果找到返回其直接引用。當多個接口時,具體實現要看具體的編譯器,有的嚴格編譯器可能會在多個接口都有匹配方法時,進行拒絕編譯。
- 如果沒找到,拋出沒有該方法異常。
⑤初始化
感覺這老師講得太爛了,不適合初學者學習。
類加載器
功能:用于根據全限定名,加載字節流到虛擬機內存中。
注意:當同一個類文件由兩個類加載器加載時,其在虛擬機中不屬于同一個類,因為每個類加載器有自己類名字列表空間。
啟動類加載器(Bootstrap ClassLoader):啟動類加載器是用C++語言實現的,是虛擬機自身的組成部分。
- 功能:用于加載存放在<JAVA_HOME>\lib目錄下的、-Xbootclasspath參數指定路徑下的、java虛擬機能夠識別的類庫到虛擬機內存中(按文件名和后綴識別)。
- 說明:無法被類加載器直接引用,當需要讓Bootstrap ClassLoader來加載類時,設置ClassLoader 實例=null即可。
擴展類加載器(Extension CLassLoader):sun.misc.Launcher$ExtClassLoader中以java代碼實現。
- 功能:用于加載存放在<JAVA_HOME>\lib\ext目錄下的、java.ext.dirs系統變量所指定的路勁下的所有類庫。用戶可以將拓展的類庫放在<JAVA_HOME>\lib\ext下來對JAVA SE拓展。
- 說明:因為拓展類加載器是由java寫成,所以可以直接在java代碼中使用拓展類加載器來加載CLass文件。
應用程序加載器(Application ClassLoader):(因其是ClassLoader.getSystemClassLoader()的返回值,也稱系統類加載器)sun.misc.Launcher$AppClassLoader中以java代碼實現。
- 功能:用于加載用戶編寫的類,所在路徑的所有類庫。
- 說明:開發者可以直接在代碼中使用這個類。并且當沒有自定義的類加載器時,其會作為默認類加載器。
雙親委派模型(Parents Delegation Model)--三層類加載器
說明:JDK9之前的java應用都是由啟動、拓展、應用類加載器互相配合完成加載。當想要從磁盤外以及其他路徑加載類或通過加載器實現類的隔離、重載等功能時,用戶可以自定義類來進行拓展。其非強制性模型,但由java官方推薦使用這個模型。
層次結構:雙親委派模型中,父子關系不是繼承,而是組合。如自定義類加載器可以組合應用程序類加載器。除了頂層的啟動類加載器外,所有的類加載器都應該要有父輩類加載器。
工作過程(先上后下):當一個類加載器收到類加載請求,其首先會向父輩類加載器傳遞這個請求,而父輩類加載器也會向其父輩類加載器傳遞,直到最頂。當父輩在自己的類加載路徑目錄下找不到該類時,就會讓子輩類加載器來加載,以此又往下傳遞。
優點:
- 保證程序穩定運作:越基礎的類由越上層的類加載器加載;這種層次結構及工作方式,讓系統類庫的類,永遠都是由固定的類加載器加載。如java.lang.Object類,處在rt.jar中,將由啟動類加載器加載。就算用戶自定義了一個同名類,但永遠也無法加載,因為每次都會加載Object類。
- 實現模型的代碼十分簡單:僅十余行代碼,先檢查請求加載的類型是否已經被加載過,若沒有則調用父加載器的loadClass()方法,若父加載器為空則默認使用啟動類加載器作為父加載器。假如父類加載器加載失敗,拋出ClassNotFoundException異常的話,才調用自己的findClass()方法嘗試進行加載。
延伸:關于熱部署等方法,以及JDBC、JNDI等"破壞"了模型,但是卻解決了問題。
java模塊化系統(Java Platform Module System,JPMS)
模塊:JDK9之后引入模塊化系統,如jar(archive,文檔)包中, 之前僅用作類庫的容器,而現在其還可以包含模塊的信息(實現封裝隔離機制)。包括:所依賴的模塊列表、導出的包列表(其他模塊可使用)、開放的包列表(其他模塊可反射訪問的列表)、使用的服務列表、提供服務的實現列表。
類路徑和模塊路徑(ModulePath):jdk9后將路徑分為類路徑和模塊路徑。類路徑上的全部以傳統jar包看待(就算包含了模塊化信息);模塊路徑上的jar或jmod文件全部以模塊看待;
訪問規則:
- JAR文件在類路徑訪問規則:將所有類路徑上的jar文件及其他資源文件,看做放在了一個匿名模塊里。其可以看到本模塊內所有包、jdk系統模塊中所有的導出包、模塊路徑上所有模塊的導出包。
- JAR文件在模塊路徑訪問規則:盡管不包含module-info.class文件,其只要在模塊路徑上,就被當做模塊對待。默認依賴于整個模塊路徑上的所有模塊(可訪問他們的導出包),且默認導出其所有的包。
- 模塊在模塊路徑的訪問規則:普通模塊稱為具名模塊(Named Module),只能訪問其所列出的依賴模塊和包,對jar文件(匿名模塊)里的所有內容不可見。
模塊化下的類加載器
變更:
-
拓展類加載器 被 平臺類加載器 取代。
因為模塊化本身具足拓展性,不需要再有<JAVA_HOME>\lib\ext目錄和系統變量java.ext.dirs和拓展類加載器了
-
<JAVA_HOME>\jre也被取消。
而jre也可以隨時通過模塊,構建出一個運行環境。如:
jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre
-
平臺類加載器 和 應用程序加載器 取消繼承自java.net.URLCLASSLoader ,轉而與啟動類加載器一起繼承自jdk.internal.loader.BuiltinClassLoader,該類實現了模塊化下的類加載邏輯,及資源可訪問性處理。
如果有程序依賴這個繼承關系,或者依賴于URLClassLoader的特定方法,那代碼可能會在JDK9及以后版本中崩潰。
如上述,啟動類加載器也變成虛擬機內部和java類庫共同協作實現的類加載器了。但是調用方式還是要自定義類加載器并賦值為null.
雙親委派模型中,類加載器關系也發生變化。
系統模塊:
歸屬:系統模塊有規定的類加載器,當加載一個模塊時,在向父輩傳遞請求之前,先判斷是否是系統模塊,及其歸屬的類加載器是哪個,并將請求交給他。若非系統模塊,才交給父類傳遞。
BootStrap ClassLoader負責加載的模塊:
java.base | java.datatransfer | java.desktop | java.instrement |
---|---|---|---|
java.logging | java.management | java.management.rmi | java.naming |
java.prefs | java.rmi(遠程方法調用) | java.security.sasl | java.xml |
jdk.httpserver | jdk.internal.vm.ci | jdk.management | jdk.management.agent |
jdk.naming.rmi | jdk.net | jdk.sctp | jdk.unsupported |
Platform ClassLoader負責加載的模塊:
crypto(加密)、incubator(孵化器)
Application ClassLoader負責加載的模塊
虛擬機字節碼執行引擎
概述:與物理機不同,虛擬機用軟件層面的執行引擎,來對二進制字節碼流進行處理。通常執行引擎運作方式為:解釋執行和編譯執行。不同虛擬機實現中,選擇的方法可能不同,單一或者搭配,或者按等級結構分配執行引擎。
運行時棧幀結構
棧幀:儲存了函數方法的局部變量、操作數棧、動態連接、方法返回地址等。不同棧幀作為不同方法的所有物,是完全獨立的。
棧幀生命周期:從調用一個方法開始,到執行結束的過程。也是其在虛擬機棧里,入棧到出棧的過程。
當前幀棧:對執行引擎來說,只有最頂的幀棧是正在執行的幀棧,引擎所執行的所有字節碼都只針對該幀棧進行操作。
細節:
- 棧幀中的局部變量表所需空間、棧深度,都已經在編譯的時候確定并寫入Code表中。運行時不會改變。
棧幀結構示意圖:
局部變量表:
組成:多個槽構成。用于存儲 方法參數 和 方法內部定義的局部變量。
槽:《java虛擬機規范》并沒有規定其大小,而是只要能大于等于32位就行(放下32位以內的所有類型)。64位分成兩個,高位在前。
虛擬機數據類型:reference=32位。注意!其和java語言數據類型不同。尤其是其中的引用reference是占一個槽,還有一個returnAddress類型也是一個槽。(returnAddress為執行一條字節碼指令的地址,不常見了,其作為古老jdk上面異常處理跳轉的助手)
儲存結構:類似于數組,當全是32位以下類型時,第n個數據,就放在第n個槽。64位數據,則占據2槽,放在n、n+1位(高位在前)。
64位的數據,虛擬機不允許任何方式單獨訪問其中一個槽。校驗階段會發生異常。
方法調用過程:(對于實參到形參的傳遞,使用棧幀的局部變量表來完成;運行調用時,生成如下過程)
- 1.如果執行的是對象實例的方法(不是直接Class.(static)function),則局部變量表中,第0個槽,即引用==0位置的槽,會放置上其所屬對象的引用。
- 2.從第一個槽開始,依實參順序依次放入變量表中。
- 3.然后依方法內的局部變量先后順序及作用域,將他們依次放入變量表中。
局部變量表槽復用:在一個函數方法內,當有多個作用域時,槽的數量不一定就是所有變量數量和,而是可以進行復用。如有多個"{...}"區域存在,其中的代碼的作用域到"}"為止。(疑問:意思是不一次放入所有變量到局部變量表么?)
操作數棧:
組成:一個后進先出的棧數據結構。最大棧深度,編譯時已被寫入Code屬性的max_stacks數據項中。
- 棧元素可包括64位,其占兩個棧容量。普通32位占一個棧容量。
功能:用于完成字節碼中的操作。如:iadd指令,執行時會將操作數棧頂兩個元素進行出棧,并相加,隨后壓入棧中。其前面要有類似兩個iload指令,將int整數放入棧中。
- 注意:虛擬機字節碼指令執行,對數據要求是絕對匹配的。如iadd,操作棧中的棧頂兩個元素必須是int型,不能是float、double、long等。否則編譯階段就會報錯,就算通過或手寫字節碼,虛擬機驗證階段也會報錯。
操作數棧共享
說明:出于節約空間和共享數據理念,有部分操作數棧區域可以共享。
動態連接
組成:一個引用。其指向運行時方法區:常量池里,該棧幀所屬的方法。
方法返回地址:
組成:保存返回地址。
說明:動態鏈接、方法返回地址、其他附加信息等組合起來稱為棧幀信息。
方法調用
解析:當虛擬機進行解析操作時,只有類里的static、private方法會被解析成直接引用,因為其不會在運行時更改了。
方法調用指令集:
- invokestatic:調用靜態方法
- invokespecial:調用實例構造器< init>()方法、私有方法和父類中的方法。
- invokevirtual:用于調用所有的虛方法。
- invokeinterface:調用接口方法
- invokedynamic:在運行時,動態解析出調用點的限定符所引用的方法,然后執行該方法。
invokestatic和invokespecial都可以在解析時,確定唯一的調用版本(方法的內部結構),兩個指令集對應的方法,其符號引用在解析時就生成了直接引用。
非虛方法:靜態方法、實例構造器<init>()方法、私有方法、父類方法、final修飾的方法。
虛方法:所有其他方法。
分派
靜態類型:是編譯期間確定的。注意:即使Human human=new Man(),但是human的類型還是Human,除非強制轉換,只有到運行時,才會確定其變成Man類型。
實際類型:(actual type)編譯期間是無法確定其類型的,只有運行時才可以。
// 實際類型變化
Human human = (new Random()).nextBoolean() ? new Man() : new Woman();
// 靜態類型變化
sr.sayHello((Man) human)
sr.sayHello((Woman) human)
靜態分派:依靜態類型,來決定方法執行版本的分派動作。發生在編譯階段。最典型應用是方法重載。
- 自動轉型順序:char>int>long>float>double。(調用方法,當輸入的參數不對應其已有重載方法時。如:參數為'a',若無fun(char x)類型,則轉化為unicode整數,或繼續轉化成長整數...)
動態分派:(*難!書里8.3.2)
單分派和多分派:目前來說,靜態多分派,動態單分派。
動態類型語言支持
靜態和動態類型語言判定:類型檢查是在編譯期間還是運行期間?
- 類型檢查:如:obj.fun(),靜態類型語言在編譯期間,會確定obj的靜態類型,而且其運行時實際類型必須為其本身或派生。而動態類型語言不會確定obj的本身類型,只會在運行時確定其的實際類型。
- 連接時、運行時異常:連接時是在類加載階段就報錯。運行時異常是只有運行到異常位置才會報錯,能通過類加載。
注意:動態類型語言與動態語言、弱類型語言并不是一個概念,需要區別對待。
根本學不懂,后面在學吧。
基于棧的字節碼解釋執行引擎
tomcatd的目錄組織及自定義類加載器
說明:(Tomcat 6之前的結構)定義了多個目錄,提供不同權限,并實現類庫隔離。(Common、Shared、WebApp\WEB_INF、Server(Catalina類加載器))
每一個WebApp類加載器和JSP類加載器通常會存在多個實例。
Common類加載器能加載的類都可以被Catalina類加載器和Shared類加載器使用,而Catalina類加載器和Shared類加載器自己能加載的類則與對方相互隔離。
JasperLoader的加載范圍僅僅是這個JSP文件所編譯出來的那一個Class文件,它存在的目的就是為了被丟棄:當服務器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,并通過再建立一個新的JSP類加載器來實現JSP文件的HotSwap功能
注意:在Tomcat 6及之后的版本簡化了默認的目錄結構(/common、/shared、/server合并在/bin目錄),只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader項后才會真正建立Catalina類加載器和Shared類加載器的實例,否則會用到這兩個類加載器的地方都會用Common類加載器的實例代替