一扣泊、jvm數(shù)據(jù)區(qū)域
1.Java虛擬機(jī)在執(zhí)行Java程序的過程中會把它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域肝断。
程序計數(shù)器:
(1) java多線程中是通過線程切換的來實現(xiàn)的停做,在切換到下一個線程過程中需要記錄當(dāng)前線程的正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址(執(zhí)行Native方法時計數(shù)器為Undefined)猜拾,CPU切換回來時會按照計數(shù)器記錄的行數(shù)繼續(xù)執(zhí)行唯绍。
(2) 每一個線程都有自己獨自的程序計數(shù)器、并且是獨享偿荷、互不影響的窘游。
(3) 此內(nèi)存區(qū)域是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。
java虛擬機(jī)棧
(1)虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀(Stack Frame[插圖])用于存儲局部變量表遭顶、操作棧张峰、動態(tài)鏈接泪蔫、方法出口等信息棒旗。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中從入棧到出棧的過程撩荣。
(2)在Java虛擬機(jī)規(guī)范中铣揉,對這個區(qū)域規(guī)定了兩種異常狀況:如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常餐曹;如果虛擬機(jī)椆涔埃可以動態(tài)擴(kuò)展(當(dāng)前大部分的Java虛擬機(jī)都可動態(tài)擴(kuò)展,只不過Java虛擬機(jī)規(guī)范中也允許固定長度的虛擬機(jī)棧)台猴,當(dāng)擴(kuò)展時無法申請到足夠的內(nèi)存時會拋出OutOfMemoryError異常朽合。
本地方法棧:
本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)饱狂,而本地方法棧則是為虛擬機(jī)使用到的Native方法服務(wù)曹步。
java堆:
(1) 對象的實例、數(shù)組都是存儲在此內(nèi)存區(qū)域休讳。
(2) 它是java垃圾收集器管理的區(qū)域讲婚。
(3) 如果在堆中沒有內(nèi)存完成實例分配,并且堆也無法再擴(kuò)展時俊柔,將會拋出OutOfMemoryError異常筹麸。
方法區(qū):
方法區(qū)(Method Area)與Java堆一樣活合,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機(jī)加載的類信息物赶、常量白指、靜態(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)存缸血。
二、垃圾回收
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ū)(永久代)的垃圾回收分為“廢棄的常量波俄、無用的類”晨逝。回收廢棄的常量和回收堆中的對象類似弟断、如果沒有對這個常量的引用之后就會被回收咏花。無用的類回收起來很苛刻如下:
(1) 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例。
(2) 加載該類的ClassLoader已經(jīng)被回收昏翰。
(3) 該類對應(yīng)的java.lang.Class 對象沒有在任何地方被引用苍匆,無法在任何地方通過反射訪問該類的方法。
在大量使用反射棚菊、動態(tài)代理浸踩、CGLib等bytecode框架的場景,以及動態(tài)生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機(jī)具備類卸載的功能统求,以保證永久代不會溢出检碗。
3.垃圾收集算法:
3.1標(biāo)記清除算法:
標(biāo)記-清除算法采用從根集合(GC Roots)進(jìn)行掃描,對存活的對象進(jìn)行標(biāo)記码邻,標(biāo)記完畢后折剃,再掃描整個空間中未被標(biāo)記的對象,進(jìn)行回收像屋,如下圖所示怕犁。標(biāo)記-清除算法不需要進(jìn)行對象的移動,只需對不存活的對象進(jìn)行處理己莺,在存活對象比較多的情況下極為高效奏甫,但由于標(biāo)記-清除算法直接回收不存活的對象,因此會造成內(nèi)存碎片凌受。
3.2復(fù)制算法:
它將可用內(nèi)存按容量劃分為大小相等的兩塊阵子,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了胜蛉,就將還存活著的對象復(fù)制到另外一塊上面挠进,然后再把已使用的內(nèi)存空間一次清理掉,這樣一來就不容易出現(xiàn)內(nèi)存碎片的問題腾么。
很顯然奈梳,Copying算法的效率跟存活對象的數(shù)目多少有很大的關(guān)系,如果存活對象很多解虱,那么Copying算法的效率將會大大降低。
3.3標(biāo)記整理算法:
該算法標(biāo)記階段和Mark-Sweep一樣漆撞,但是在完成標(biāo)記之后殴泰,它不是直接清理可回收對象,而是將存活對象都向一端移動
*分代收集算法:
根據(jù)對象的存活周期的不同將內(nèi)存劃分為幾塊浮驳。一般是把Java堆分為新生代和老年代悍汛,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴?/p>
jvm將新生代分為三個區(qū)域、一個end區(qū)和兩個suvivor區(qū)至会。新生代的采用復(fù)制算法离咐。新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活宵蛀,那就選用復(fù)制算法昆著,只需要付出少量存活對象的復(fù)制成本就可以完成收集。而老年代中因為對象存活率高术陶、沒有額外空間對它進(jìn)行分配擔(dān)保凑懂,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來進(jìn)行回收。
4.垃圾回收策略:
4.1. 對象優(yōu)先在 Eden 分配
大多數(shù)情況下梧宫,對象在新生代 Eden 區(qū)分配接谨,當(dāng) Eden 區(qū)空間不夠時,發(fā)起 Minor GC塘匣。
4.2. 大對象直接進(jìn)入老年代
大對象是指 需要連續(xù)內(nèi)存空間的對象 脓豪,最典型的大對象是那種很長的字符串以及數(shù)組。
經(jīng)常出現(xiàn)大對象會 提前觸發(fā)垃圾收 集以獲取足夠的連續(xù)空間分配給大對象忌卤。
4.3. 長期存活的對象進(jìn)入老年代
為對象定義年齡計數(shù)器跑揉,對象在 Eden 出生并經(jīng)過 Minor GC 依然存活, 將移動到 Survivor 中埠巨,年齡就增加 1 歲历谍,增加到一定年齡則移動到老年代中。
-XX:MaxTenuringThreshold 用來定義年齡的閾值辣垒。
4.4. 動態(tài)對象年齡判定
虛擬機(jī)并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到 MaxTenuringThreshold 才能晉升老年代望侈, 如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半, 則年齡大于或等于該年齡的對象可以直接進(jìn)入老年代勋桶,無需等到 MaxTenuringThreshold 中要求的年齡脱衙。
4.5. 空間分配擔(dān)保
在發(fā)生 Minor GC 之前,虛擬機(jī)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間例驹,如果條件成立的話捐韩,那么 Minor GC 可以確認(rèn)是安全的。
三鹃锈、虛擬機(jī)性能監(jiān)控與故障處理工具
虛擬機(jī)進(jìn)程狀況工具:
-
jps命令行工具:
jps是jdk提供的一個查看當(dāng)前java進(jìn)程的小工具荤胁, 可以看做是JavaVirtual Machine Process Status Tool的縮寫尤溜。
示例:
jps –l:輸出主類或者jar的完全路徑名:
image.png
jps –v :輸出jvm參數(shù):
2.jstat:虛擬機(jī)統(tǒng)計信息監(jiān)視工具:
是用于監(jiān)視虛擬機(jī)各種運行狀態(tài)信息的命令行工具后众。它可以顯示本地或遠(yuǎn)程[插圖]虛擬機(jī)進(jìn)程中的類裝載、內(nèi)存静汤、垃圾收集盆驹、JIT編譯等運行數(shù)據(jù)圆丹,在沒有GUI圖形界面,只提供了純文本控制臺環(huán)境的服務(wù)器上躯喇,它將是運行期定位虛擬機(jī)性能問題的首選工具辫封。
假設(shè)需要每250毫秒查詢一次進(jìn)程2764垃圾收集的狀況,一共查詢20次,那命令應(yīng)當(dāng)是:
jstat -gc 12008 250 20
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
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:用于生成虛擬機(jī)當(dāng)前時刻的線程快照(一般稱為threaddump或javacore文件)劣欢。線程快照就是當(dāng)前虛擬機(jī)內(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程序
四、類文件結(jié)構(gòu)
1.class文件結(jié)構(gòu)
1.1.Class文件是一組以8個字節(jié)為基礎(chǔ)單位的二進(jìn)制流(可能是磁盤文件获枝,也可能是類加載器直接生成的)蠢正,各個數(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é)慎冤,唯一作用是確定文件是否為一個可被虛擬機(jī)接受的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代碼在進(jìn)行javac編譯的時候并不像C和C++那樣有連接這一步沿后,而是在虛擬機(jī)加載class文件的時候進(jìn)行動態(tài)連接沿彭。也就是說,在class文件中不會保存各個方法得运、字段的最終內(nèi)存布局信息膝蜈,因此這些字段、方法的符號引用不經(jīng)過運行期轉(zhuǎn)換的話無法得到真正的內(nèi)存入口地址熔掺,虛擬機(jī)也就無法使用饱搏。當(dāng)虛擬機(jī)運行時,需要從常量池獲得對應(yīng)的符號引用置逻,再在類創(chuàng)建時或運行時解析推沸、翻譯到具體的內(nèi)存地址中。
常量池中的每一項都是一個表券坞,這14個表的開始第一個字節(jié)是一個u1類型的tag鬓催,用來標(biāo)識是哪一種常量類型。這14種常量類型所代表的含義如下:
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ù)組)以及字段名稱。
五吃谣、虛擬機(jī)加載機(jī)制
上一節(jié)我們已經(jīng)知道了類文件結(jié)構(gòu)乞封,在class文件中描述的各種信息最終都需要加載到虛擬機(jī)中之后才能運行和使用。
1.類的加載過程:
包括加載岗憋、驗證肃晚、準(zhǔn)備、解析仔戈、初始化关串。
1.1加載:
在加載階段,虛擬機(jī)需要完成以下三件事情:
1)通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(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)前虛擬機(jī)的要求墓卦,并且不會危害虛擬機(jī)自身的安全。
1.2.1.為什么需要驗證:
Class文件并不一定要求用Java源碼編譯而來户敬,可以使用任何途徑趴拧,包括用十六進(jìn)制編輯器直接編寫來產(chǎn)生Class文件溅漾。在字節(jié)碼的語言層面上山叮,上述Java代碼無法做到的事情都是可以實現(xiàn)的著榴,至少語義上是可以表達(dá)出來的。虛擬機(jī)如果不檢查輸入的字節(jié)流屁倔,對其完全信任的話脑又,很可能會因為載入了有害的字節(jié)流而導(dǎo)致系統(tǒng)崩潰,所以驗證是虛擬機(jī)對自身保護(hù)的一項重要工作锐借。
1.2.2.過程:
大致上都會完成下面四個階段的檢驗過程:文件格式驗證问麸、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證钞翔。
2.準(zhǔn)備:
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段
- 解析:
解析階段是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程
3.1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標(biāo)严卖,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可布轿。符號引用與虛擬機(jī)實現(xiàn)的內(nèi)存布局無關(guān)哮笆,引用的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。
3.2. 直接引用(Direct References):直接引用可以是直接指向目標(biāo)的指針汰扭、相對偏移量或是一個能間接定位到目標(biāo)的句柄稠肘。直接引用是與虛擬機(jī)實現(xiàn)的內(nèi)存布局相關(guān)的,同一個符號引用在不同虛擬機(jī)實例上翻譯出來的直接引用一般不會相同萝毛。如果有了直接引用项阴,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
4.初始化:
4.1.遇到new笆包、getstatic环揽、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進(jìn)行過初始化庵佣,則需要先觸發(fā)其初始化歉胶。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾秧了、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候跨扮,以及調(diào)用一個類的靜態(tài)方法的時候。
4.2.使用java.lang.reflect包的方法對類進(jìn)行發(fā)射調(diào)用的時候验毡,如果類沒有進(jìn)行過初始化衡创,則需要先觸發(fā)其初始化。
4.3當(dāng)初始化一個類的時候晶通,如果發(fā)現(xiàn)其父類還沒有進(jìn)行過初始化璃氢,則需要先觸發(fā)其父類的初始化。
4.4當(dāng)虛擬機(jī)啟動時狮辽,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)一也,虛擬機(jī)會先初始化這個主類巢寡。
對于這四種會觸發(fā)類進(jìn)行初始化的場景,虛擬機(jī)規(guī)范中使用了一個很強烈的限定語:“有且只有”椰苟,這四種場景中的行為稱為對一個類進(jìn)行主動引用抑月。除此之外所有引用類的方式,都不會觸發(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虛擬機(jī)的角度講,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader)携茂,這個類加載器使用C++語言實現(xiàn)[插圖]你踩,是虛擬機(jī)自身的一部分;另外一種就是所有其他的類加載器讳苦,這些類加載器都由Java語言實現(xiàn)带膜,獨立于虛擬機(jī)外部,并且全都繼承自抽象類java.lang.ClassLoader鸳谜。
5.1 啟動類加載器(Bootstrap ClassLoader):前面已經(jīng)介紹過膝藕,這個類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的咐扭,并且是虛擬機(jī)識別的(僅按照文件名識別芭挽,如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中蝗肪。啟動類加載器無法被Java程序直接引用袜爪。
5.2擴(kuò)展類加載器(Extension ClassLoader):這個加載器由sun.misc.LauncherAppClassLoader來實現(xiàn)豁延。由于這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值昙篙,所以一般也稱它為系統(tǒng)類加載器腊状。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個類加載器苔可,如果應(yīng)用程序中沒有自定義過自己的類加載器缴挖,一般情況下這個就是程序中默認(rèn)的類加載器。
5.4 過程:
雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求硕蛹,它首先不會自己去嘗試加載這個類醇疼,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此法焰,因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時倔毙,子加載器才會嘗試自己去加載埃仪。
六、虛擬機(jī)執(zhí)行引擎
在Java虛擬機(jī)規(guī)范中制定了虛擬機(jī)字節(jié)碼執(zhí)行引擎的概念模型陕赃,這個概念模型成為各種虛擬機(jī)執(zhí)行引擎的統(tǒng)一外觀(Facade)卵蛉。在不同的虛擬機(jī)實現(xiàn)里面,執(zhí)行引擎在執(zhí)行Java代碼的時候可能有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇么库,也可能兩者兼?zhèn)渖邓浚踔吝€可能包含幾個不同級別的編譯器執(zhí)行引擎。但從外觀上看起來诉儒,所有的Java虛擬機(jī)的執(zhí)行引擎都是一致的:輸入的是字節(jié)碼文件葡缰,處理過程是字節(jié)碼解析的等效過程,輸出的是執(zhí)行結(jié)果忱反。本章將主要從概念模型的角度來講解虛擬機(jī)的方法調(diào)用和字節(jié)碼執(zhí)行泛释。
1.運行時棧幀結(jié)構(gòu)
棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運行時數(shù)據(jù)區(qū)中的虛擬機(jī)棧(Virtual Machine Stack)[插圖]的棧元素温算。棧幀存儲了方法的局部變量表怜校、操作數(shù)棧、動態(tài)連接和方法返回地址等信息注竿。
1.1局部變量表:
局部變量表的容量以變量槽(Variable Slot茄茁,下稱Slot)為最小單位,虛擬機(jī)規(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è)置虛擬機(jī)參數(shù)-verbose:gc看運行結(jié)果:
這是因為當(dāng)執(zhí)行system.gc的時候a參數(shù)還在當(dāng)前作用域徒恋,此時無法進(jìn)行垃圾回收蚕断。
public static void main(String[] args) {
{
byte[] a = new byte[64 * 1024 * 1024];
}
int b=0;
System.gc();
}
此時可以看到,垃圾會被回收入挣。因為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虛擬機(jī)內(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)在程序代碼寫好笆焰、編譯器進(jìn)行編譯時就必須確定下來劫谅。這類方法的調(diào)用稱為解析(Resolution)。
在Java語言中嚷掠,符合“編譯期可知,運行期不可變”這個要求的方法主要有靜態(tài)方法和私有方法兩大類荞驴,前者與類型直接關(guān)聯(lián)不皆,后者在外部不可被訪問,這兩種方法都不可能通過繼承或別的方式重寫出其他版本熊楼,因此它們都適合在類加載階段進(jìn)行解析霹娄。與之相對應(yīng),在Java虛擬機(jī)里面提供了四條方法調(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)分派的動作實際上不是由虛擬機(jī)來執(zhí)行的。
動態(tài)分派:
我們把在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派卸奉。