2020-10-12---深入理解java虛擬機總結(jié)

一、jvm數(shù)據(jù)區(qū)域
1.Java虛擬機在執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域。


image.png

程序計數(shù)器:
(1) java多線程中是通過線程切換的來實現(xiàn)的,在切換到下一個線程過程中需要記錄當(dāng)前線程的正在執(zhí)行的虛擬機字節(jié)碼指令地址(執(zhí)行Native方法時計數(shù)器為Undefined),CPU切換回來時會按照計數(shù)器記錄的行數(shù)繼續(xù)執(zhí)行。
(2) 每一個線程都有自己獨自的程序計數(shù)器、并且是獨享、互不影響的。
(3) 此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

java虛擬機棧
(1)虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀(Stack Frame[插圖])用于存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。

(2)在Java虛擬機規(guī)范中,對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機??梢詣討B(tài)擴展(當(dāng)前大部分的Java虛擬機都可動態(tài)擴展,只不過Java虛擬機規(guī)范中也允許固定長度的虛擬機棧),當(dāng)擴展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError異常。

本地方法棧:
本地方法棧(Native Method Stacks)與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native方法服務(wù)。

java堆:
(1) 對象的實例、數(shù)組都是存儲在此內(nèi)存區(qū)域。
(2) 它是java垃圾收集器管理的區(qū)域。
(3) 如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴展時,將會拋出OutOfMemoryError異常。

方法區(qū):
方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將拋出OutOfMemoryError異常。

運行時常量池:
常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運行時常量池中。

對象訪問:
簡單的Object o=new Object();首先“Object obj”這部分的語義將會反映到Java棧的本地變量表中,作為一個reference類型數(shù)據(jù)出現(xiàn)。而“newObject()”這部分的語義將會反映到Java堆中,形成一塊存儲了Object類型所有實例數(shù)據(jù)值的結(jié)構(gòu)化內(nèi)存。


image.png

二、垃圾回收
1..判斷是否是垃圾的算法:
*引用計數(shù)法:
給對象中添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加1;當(dāng)引用失效時,計數(shù)器值就減1;任何時刻計數(shù)器都為0的對象就是不可能再被使用的。
缺點:不能解決循環(huán)引用的問題。

*根搜索算法:
在主流的商用程序語言中(Java和C#,甚至包括前面提到的古老的Lisp),都是使用根搜索算法(GC Roots Tracing)判定對象是否存活的。這個算法的基本思路就是通過一系列的名為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說就是從GC Roots到這個對象不可達(dá))時,則證明此對象是不可用的。
2.方法區(qū)的垃圾回收:
方法區(qū)(永久代)的垃圾回收分為“廢棄的常量、無用的類”?;厥諒U棄的常量和回收堆中的對象類似、如果沒有對這個常量的引用之后就會被回收。無用的類回收起來很苛刻如下:
(1) 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例。
(2) 加載該類的ClassLoader已經(jīng)被回收。
(3) 該類對應(yīng)的java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
在大量使用反射、動態(tài)代理、CGLib等bytecode框架的場景,以及動態(tài)生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

3.垃圾收集算法:
3.1標(biāo)記清除算法:
標(biāo)記-清除算法采用從根集合(GC Roots)進行掃描,對存活的對象進行標(biāo)記,標(biāo)記完畢后,再掃描整個空間中未被標(biāo)記的對象,進行回收,如下圖所示。標(biāo)記-清除算法不需要進行對象的移動,只需對不存活的對象進行處理,在存活對象比較多的情況下極為高效,但由于標(biāo)記-清除算法直接回收不存活的對象,因此會造成內(nèi)存碎片。


image.png

3.2復(fù)制算法:
它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題。
很顯然,Copying算法的效率跟存活對象的數(shù)目多少有很大的關(guān)系,如果存活對象很多,那么Copying算法的效率將會大大降低。


image.png

3.3標(biāo)記整理算法:
該算法標(biāo)記階段和Mark-Sweep一樣,但是在完成標(biāo)記之后,它不是直接清理可回收對象,而是將存活對象都向一端移動
image.png

*分代收集算法:

根據(jù)對象的存活周期的不同將內(nèi)存劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴?/p>

jvm將新生代分為三個區(qū)域、一個end區(qū)和兩個suvivor區(qū)。新生代的采用復(fù)制算法。新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來進行回收。

4.垃圾回收策略:
4.1. 對象優(yōu)先在 Eden 分配
大多數(shù)情況下,對象在新生代 Eden 區(qū)分配,當(dāng) Eden 區(qū)空間不夠時,發(fā)起 Minor GC。

4.2. 大對象直接進入老年代
大對象是指 需要連續(xù)內(nèi)存空間的對象 ,最典型的大對象是那種很長的字符串以及數(shù)組。
經(jīng)常出現(xiàn)大對象會 提前觸發(fā)垃圾收 集以獲取足夠的連續(xù)空間分配給大對象。

4.3. 長期存活的對象進入老年代
為對象定義年齡計數(shù)器,對象在 Eden 出生并經(jīng)過 Minor GC 依然存活, 將移動到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動到老年代中。
-XX:MaxTenuringThreshold 用來定義年齡的閾值。

4.4. 動態(tài)對象年齡判定
虛擬機并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到 MaxTenuringThreshold 才能晉升老年代, 如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半, 則年齡大于或等于該年齡的對象可以直接進入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。

4.5. 空間分配擔(dān)保
在發(fā)生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 可以確認(rèn)是安全的。

三、虛擬機性能監(jiān)控與故障處理工具
虛擬機進程狀況工具:

  1. jps命令行工具:
    jps是jdk提供的一個查看當(dāng)前java進程的小工具, 可以看做是JavaVirtual Machine Process Status Tool的縮寫。
    示例:
    jps –l:輸出主類或者jar的完全路徑名:


    image.png

jps –v :輸出jvm參數(shù):


image.png

2.jstat:虛擬機統(tǒng)計信息監(jiān)視工具:
是用于監(jiān)視虛擬機各種運行狀態(tài)信息的命令行工具。它可以顯示本地或遠(yuǎn)程[插圖]虛擬機進程中的類裝載、內(nèi)存、垃圾收集、JIT編譯等運行數(shù)據(jù),在沒有GUI圖形界面,只提供了純文本控制臺環(huán)境的服務(wù)器上,它將是運行期定位虛擬機性能問題的首選工具。

假設(shè)需要每250毫秒查詢一次進程2764垃圾收集的狀況,一共查詢20次,那命令應(yīng)當(dāng)是:
jstat -gc 12008 250 20


image.png

S0C:第一個幸存區(qū)的大小
S1C:第二個幸存區(qū)的大小
S0U:第一個幸存區(qū)的使用大小
S1U:第二個幸存區(qū)的使用大小
EC:伊甸園區(qū)的大小
EU:伊甸園區(qū)的使用大小
OC:老年代大小
OU:老年代使用大小
MC:方法區(qū)大小
MU:方法區(qū)使用大小
CCSC:壓縮類空間大小
CCSU:壓縮類空間使用大小
YGC:年輕代垃圾回收次數(shù)
YGCT:年輕代垃圾回收消耗時間
FGC:老年代垃圾回收次數(shù)
FGCT:老年代垃圾回收消耗時間
GCT:垃圾回收消耗總時間

堆內(nèi)存統(tǒng)計:
jstat -gccapacity 12008

image.png

NGCMN:新生代最小容量
NGCMX:新生代最大容量
NGC:當(dāng)前新生代容量
S0C:第一個幸存區(qū)大小
S1C:第二個幸存區(qū)的大小
EC:伊甸園區(qū)的大小
OGCMN:老年代最小容量
OGCMX:老年代最大容量
OGC:當(dāng)前老年代大小
OC:當(dāng)前老年代大小
MCMN:最小元數(shù)據(jù)容量
MCMX:最大元數(shù)據(jù)容量
MC:當(dāng)前元數(shù)據(jù)空間大小
CCSMN:最小壓縮類空間大小
CCSMX:最大壓縮類空間大小
CCSC:當(dāng)前壓縮類空間大小
YGC:年輕代gc次數(shù)
FGC:老年代GC次數(shù)

3.jmap:(Memory Map for Java)用于生成堆轉(zhuǎn)儲快照,即Dump文件;還能查詢finalize執(zhí)行隊列、java堆和永久代詳細(xì)信息,比如空間使用率、當(dāng)前用的是哪種收集器等。
*jstasck:用于生成虛擬機當(dāng)前時刻的線程快照(一般稱為threaddump或javacore文件)。線程快照就是當(dāng)前虛擬機內(nèi)每一條線程正在執(zhí)行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現(xiàn)長時間停頓的原因,如線程間死鎖、死循環(huán)、請求外部資源導(dǎo)致的長時間等待等都是導(dǎo)致線程長時間停頓的常見原因。

jdk可視化工具:
JDK中除了提供大量的命令行工具外,還有兩個功能強大的可視化工具:JConsole和VisualVM,這兩個工具是JDK的正式成員,沒有被貼上“unsupported andexperimental”的標(biāo)簽。

4.JConsole:
它是一款基于JMX的可視化監(jiān)視和管理的工具。直接在jdk bin目錄下啟動JConsole程序


image.png
image.png

四、類文件結(jié)構(gòu)
1.class文件結(jié)構(gòu)
1.1.Class文件是一組以8個字節(jié)為基礎(chǔ)單位的二進制流(可能是磁盤文件,也可能是類加載器直接生成的),各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊地排列,中間沒有任何分隔符;
1.2.Class文件格式采用一種類似于C語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲數(shù)據(jù),其中只有兩種數(shù)據(jù)類型:無符號數(shù)和表;
1.3.無符號數(shù)屬于基本的數(shù)據(jù)類型,以u1、u2、u4和u8來分別代表1個字節(jié)、2個字節(jié)、4個字節(jié)和8個字節(jié)的無符號數(shù),可以用來描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構(gòu)成字符串值;
表是由多個無符號數(shù)獲取其他表作為數(shù)據(jù)項構(gòu)成的復(fù)合數(shù)據(jù)類型,習(xí)慣以“_info”結(jié)尾;
1.4.無論是無符號數(shù)還是表,當(dāng)需要描述同一個類型但數(shù)量不定的多個數(shù)據(jù)時,經(jīng)常會使用一個前置的容量計數(shù)器加若干個連續(xù)的數(shù)據(jù)項的形式,這時稱這一系列連續(xù)的某一類型的數(shù)據(jù)為某一類型的集合。

2.具體的類文件結(jié)構(gòu)(下面是簡單的打印語句的class文件)
2.1、魔數(shù)和版本
2.1.1. Class文件的頭4個字節(jié),唯一作用是確定文件是否為一個可被虛擬機接受的Class文件,固定為“0xCAFEBABE”。
2.1.2 第5和第6個字節(jié)是次版本號,第7和第8個字節(jié)是主版本號(0x0034為52,對應(yīng)JDK版本1.8);Java的版本號是從45開始的,JDK1.1之后的每一個JDK大版本發(fā)布主版本號向上加1,高版本的JDK能向下兼容低版本的JDK。

對應(yīng)到class文件中就是:


這里寫圖片描述

2.2常量池
2.2.1常量池的組成:
常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近Java語言的常量概念,如文本字符串、聲明為final的常量等。而符號引用則屬于編譯原理方面的概念,它包括三方面的內(nèi)容:
類和接口的全限定名(Fully Qualified Name);
字段的名稱和描述符(Descriptor);
方法的名稱和描述符;
Java代碼在進行javac編譯的時候并不像C和C++那樣有連接這一步,而是在虛擬機加載class文件的時候進行動態(tài)連接。也就是說,在class文件中不會保存各個方法、字段的最終內(nèi)存布局信息,因此這些字段、方法的符號引用不經(jīng)過運行期轉(zhuǎn)換的話無法得到真正的內(nèi)存入口地址,虛擬機也就無法使用。當(dāng)虛擬機運行時,需要從常量池獲得對應(yīng)的符號引用,再在類創(chuàng)建時或運行時解析、翻譯到具體的內(nèi)存地址中。
常量池中的每一項都是一個表,這14個表的開始第一個字節(jié)是一個u1類型的tag,用來標(biāo)識是哪一種常量類型。這14種常量類型所代表的含義如下:


image.png

2.3、訪問標(biāo)志
常量池結(jié)束后緊接著的兩個字節(jié)代表訪問標(biāo)志,用來標(biāo)識一些類或接口的訪問信息,包括:這個Class是類還是接口;是否定義為public;是否定義為abstract;如果是類的話,是否被聲明為final等。具體的標(biāo)志位以及含義如下表:


這里寫圖片描述

2.4.類索引、父類索引與接口索引集合
在訪問標(biāo)志access_flags后接下來就是類索引(this_class)和父類索引(super_class),這兩個數(shù)據(jù)都是u2類型的,而接下來的接口索引集合是一個u2類型的集合,class文件由這三個數(shù)據(jù)項來確定類的繼承關(guān)系。

2.5.字段表集合
字段表集合,顧名思義就是Java類中的字段,字段又分為類字段(靜態(tài)屬性)和實例字段(對象屬性),那么,在Class文件中是如何保存這些字段的呢?我們可以想一想保存一個字段需要保存它的哪些信息呢?
答案是:字段的作用域(public、private和protected修飾符)、是實例變量還是類變量(static修飾符)、可變性(final修飾符)、并發(fā)可見性(volatile修飾符)、是否可被序列化(transient修飾符)、字段的數(shù)據(jù)類型(基本類型、對象、數(shù)組)以及字段名稱。

五、虛擬機加載機制
上一節(jié)我們已經(jīng)知道了類文件結(jié)構(gòu),在class文件中描述的各種信息最終都需要加載到虛擬機中之后才能運行和使用。
1.類的加載過程:
包括加載、驗證、準(zhǔn)備、解析、初始化。
1.1加載:
在加載階段,虛擬機需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
3)在Java堆中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這些數(shù)據(jù)的訪問入口。

1.2驗證:
這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全。
1.2.1.為什么需要驗證:
Class文件并不一定要求用Java源碼編譯而來,可以使用任何途徑,包括用十六進制編輯器直接編寫來產(chǎn)生Class文件。在字節(jié)碼的語言層面上,上述Java代碼無法做到的事情都是可以實現(xiàn)的,至少語義上是可以表達(dá)出來的。虛擬機如果不檢查輸入的字節(jié)流,對其完全信任的話,很可能會因為載入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗證是虛擬機對自身保護的一項重要工作。
1.2.2.過程:
大致上都會完成下面四個階段的檢驗過程:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證。

2.準(zhǔn)備:
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段

  1. 解析:
    解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程
    3.1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。

3.2. 直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針、相對偏移量或是一個能間接定位到目標(biāo)的句柄。直接引用是與虛擬機實現(xiàn)的內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。

4.初始化:
4.1.遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
4.2.使用java.lang.reflect包的方法對類進行發(fā)射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
4.3當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化。
4.4當(dāng)虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

對于這四種會觸發(fā)類進行初始化的場景,虛擬機規(guī)范中使用了一個很強烈的限定語:“有且只有”,這四種場景中的行為稱為對一個類進行主動引用。除此之外所有引用類的方式,都不會觸發(fā)初始化,稱為被動引用。下面舉三個例子來說明被動引用,分別見代碼清單7-1、代碼清單7-2和代碼清單7-3。
7-1:

package com.shuai.Util;

class  father{
static {
    System.out.println("father");
}
 public  static  String name="shuai";

}
class  children extends  father{
 static {
     System.out.println("children");
 }



}


public class test3 {
 public static void main(String[] args) {
     System.out.println(children.name);
 }

}

上面運行的結(jié)果是father shuai不會運行children。當(dāng)通過子類調(diào)用父類的靜態(tài)代碼時不會初始化本身的靜態(tài)代碼塊。

7-2:

public class test3 {
    public static void main(String[] args) {
        children[] c=new children[10];
    }
}

使用數(shù)組的時候不會初始化。

7-3:

class  father{
   static {
       System.out.println("father");
   }
    public  static  final  String HELLOWORD="hello world";

}


public class test3 {
    public static void main(String[] args) {
        System.out.println(father.HELLOWORD);
    }

}

上述代碼運行之后,也沒有輸出“father”,這是因為雖然在Java源碼中引用了father類中的常量HELLOWORLD,但是在編譯階段將此常量的值“hello world”存儲到了thest類的常量池中,對常量father.HELLOWORLD的引用實際都被轉(zhuǎn)化為test類對自身常量池的引用了。也就是說實際上father的Class文件之中并沒有father類的符號引用入口,這兩個類在編譯成Class之后就不存在任何聯(lián)系了。

5..雙親委派:
站在Java虛擬機的角度講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現(xiàn)[插圖],是虛擬機自身的一部分;另外一種就是所有其他的類加載器,這些類加載器都由Java語言實現(xiàn),獨立于虛擬機外部,并且全都繼承自抽象類java.lang.ClassLoader。
5.1 啟動類加載器(Bootstrap ClassLoader):前面已經(jīng)介紹過,這個類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機內(nèi)存中。啟動類加載器無法被Java程序直接引用。
5.2擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.LauncherExtClassLoader實現(xiàn),它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴展類加載器。 5.3 應(yīng)用程序類加載器(Application ClassLoader):這個類加載器由sun.misc. LauncherAppClassLoader來實現(xiàn)。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。

5.4 過程:
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。

六、虛擬機執(zhí)行引擎
在Java虛擬機規(guī)范中制定了虛擬機字節(jié)碼執(zhí)行引擎的概念模型,這個概念模型成為各種虛擬機執(zhí)行引擎的統(tǒng)一外觀(Facade)。在不同的虛擬機實現(xiàn)里面,執(zhí)行引擎在執(zhí)行Java代碼的時候可能有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇,也可能兩者兼?zhèn)?,甚至還可能包含幾個不同級別的編譯器執(zhí)行引擎。但從外觀上看起來,所有的Java虛擬機的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件,處理過程是字節(jié)碼解析的等效過程,輸出的是執(zhí)行結(jié)果。本章將主要從概念模型的角度來講解虛擬機的方法調(diào)用和字節(jié)碼執(zhí)行。
1.運行時棧幀結(jié)構(gòu)
棧幀(Stack Frame)是用于支持虛擬機進行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機運行時數(shù)據(jù)區(qū)中的虛擬機棧(Virtual Machine Stack)[插圖]的棧元素。棧幀存儲了方法的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息。
1.1局部變量表:
局部變量表的容量以變量槽(Variable Slot,下稱Slot)為最小單位,虛擬機規(guī)范中并沒有明確指明一個Slot應(yīng)占用的內(nèi)存空間大小,只是很有“導(dǎo)向性”地說明每個Slot都應(yīng)該能存放一個boolean、byte、char、short、int、float、reference或returnAddress類型的數(shù)據(jù)。
局部變量表中的Slot是可重用的,方法體中定義的變量,其作用域并不一定會覆蓋整個方法體,如果當(dāng)前字節(jié)碼PC計數(shù)器的值已經(jīng)超出了某個變量的作用域,那么這個變量對應(yīng)的Slot就可以交給其他變量使用。這樣的設(shè)計不僅僅是為了節(jié)省棧空間,在某些情況下Slot的復(fù)用會直接影響到系統(tǒng)的垃圾收集行為。
示例:

public class Test4 {
   public static void main(String[] args) {
           byte [] a=new byte[64*1024*1024];
           System.gc();
   }
}

通過設(shè)置虛擬機參數(shù)-verbose:gc看運行結(jié)果:


image.png

這是因為當(dāng)執(zhí)行system.gc的時候a參數(shù)還在當(dāng)前作用域,此時無法進行垃圾回收。

 public static void main(String[] args) {
       {
           byte[] a = new byte[64 * 1024 * 1024];

       }
       int b=0;
       System.gc();
       }
image.png

此時可以看到,垃圾會被回收。因為slot被b復(fù)用并且清空。
1.2操作數(shù)棧:
操作數(shù)棧也常被稱為操作棧,它是一個后入先出(Last In First Out,LIFO)棧。同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數(shù)據(jù)項之中。操作數(shù)棧的每一個元素可以是任意的Java數(shù)據(jù)類型,包括long和double。32位數(shù)據(jù)類型所占的棧容量為1,64位數(shù)據(jù)類型所占的棧容量為2。在方法執(zhí)行的任何時候,操作數(shù)棧的深度都不會超過在max_stacks數(shù)據(jù)項中設(shè)定的最大值。
1.3動態(tài)鏈接
每個棧幀都包含一個指向運行時常量池[插圖]中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。
1.4方法返回地址
當(dāng)一個方法被執(zhí)行后,有兩種方式退出這個方法。第一種方式是執(zhí)行引擎遇到任意一個方法返回的字節(jié)碼指令,這時候可能會有返回值傳遞給上層的方法調(diào)用者
另外一種退出方式是,在方法執(zhí)行過程中遇到了異常,并且這個異常沒有在方法體內(nèi)得到處理,無論是Java虛擬機內(nèi)部產(chǎn)生的異常,還是代碼中使用athrow字節(jié)碼指令產(chǎn)生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會導(dǎo)致方法退出。
一般來說,方法正常退出時,調(diào)用者的PC計數(shù)器的值就可以作為返回地址,棧幀中很可能會保存這個計數(shù)器值。而方法異常退出時,返回地址是要通過異常處理器表來確定的,棧幀中一般不會保存這部分信息。
2.方法調(diào)用:
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法),暫時還不涉及方法內(nèi)部的具體運行過程。
2.1.解析:
在類加載的解析階段,會將其中的一部分符號引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是:方法在程序真正運行之前就有一個可確定的調(diào)用版本,并且這個方法的調(diào)用版本在運行期是不可改變的。換句話說,調(diào)用目標(biāo)在程序代碼寫好、編譯器進行編譯時就必須確定下來。這類方法的調(diào)用稱為解析(Resolution)。
在Java語言中,符合“編譯期可知,運行期不可變”這個要求的方法主要有靜態(tài)方法和私有方法兩大類,前者與類型直接關(guān)聯(lián),后者在外部不可被訪問,這兩種方法都不可能通過繼承或別的方式重寫出其他版本,因此它們都適合在類加載階段進行解析。與之相對應(yīng),在Java虛擬機里面提供了四條方法調(diào)用字節(jié)碼指令[插圖],分別是:□ invokestatic:調(diào)用靜態(tài)方法。□ invokespecial:調(diào)用實例構(gòu)造器<init>方法、私有方法和父類方法?!?invokevirtual:調(diào)用所有的虛方法?!?invokeinterface:調(diào)用接口方法,會在運行時再確定一個實現(xiàn)此接口的對象。只要能被invokestatic和invokespecial指令調(diào)用的方法,都可以在解析階段確定唯一的調(diào)用版本,符合這個條件的有靜態(tài)方法、私有方法、實例構(gòu)造器和父類方法四類,它們在類加載的時候就會把符號引用解析為該方法的直接引用。這些方法可以稱為非虛方法,與之相反,其他方法就稱為虛方法。

2.2分派:
解析調(diào)用是一個靜態(tài)的過程,在編譯期間就完全確定,不會延遲到運行期再去完成。
分派調(diào)用則可能是靜態(tài)的也可能是動態(tài)的。
根據(jù)分派宗數(shù)量可分為單分派和多分派。這兩類分派又可兩兩組合成:靜態(tài)單分派,靜態(tài)多分派,動態(tài)單分派和動態(tài)多分派4中分派組合。
分派體現(xiàn)了Java的多態(tài)性,如“重載”和“重寫”。
靜態(tài)分派:
所有依賴靜態(tài)類型(類型的引用)來定位方法執(zhí)行版本的分派動作,都稱為靜態(tài)分派。靜態(tài)分派的最典型應(yīng)用就是方法重載。靜態(tài)分派發(fā)生在編譯階段,因此確定靜態(tài)分派的動作實際上不是由虛擬機來執(zhí)行的。
動態(tài)分派:
我們把在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容