From:深入理解Java虛擬機(jī)
- 目錄
BiBi - JVM -0- 開(kāi)篇
BiBi - JVM -1- Java內(nèi)存區(qū)域
BiBi - JVM -2- 對(duì)象
BiBi - JVM -3- 垃圾收集算法
BiBi - JVM -4- HotSpot JVM
BiBi - JVM -5- 垃圾回收器
BiBi - JVM -6- 回收策略
BiBi - JVM -7- Java類(lèi)文件結(jié)構(gòu)
BiBi - JVM -8- 類(lèi)加載機(jī)制
BiBi - JVM -9- 類(lèi)加載器
BiBi - JVM -10- 虛擬機(jī)字節(jié)碼
BiBi - JVM -11- 編譯期優(yōu)化
BiBi - JVM -12- 運(yùn)行期優(yōu)化
BiBi - JVM -13- 并發(fā)
JIT【Just In Time】出現(xiàn)的原因
Java程序最初通過(guò)解釋器進(jìn)行解釋執(zhí)行蹋半,當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊的運(yùn)行特別頻繁時(shí)垮衷,就會(huì)把這些代碼定為【熱點(diǎn)代碼】。為了提高熱點(diǎn)代碼的執(zhí)行效率册招,在運(yùn)行時(shí)抖单,虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺(tái)相關(guān)的機(jī)器碼萎攒,并進(jìn)行各種層次的優(yōu)化,完成這個(gè)任務(wù)的編譯器就成為即時(shí)編譯器矛绘。
JIT并不是虛擬機(jī)的必需部分耍休,但即時(shí)編譯器【性能的好壞、代碼的優(yōu)化程度】卻是衡量一款虛擬機(jī)的關(guān)鍵指標(biāo)之一货矮。
Java虛擬機(jī)規(guī)范中沒(méi)有具體約束和限制即時(shí)編譯器應(yīng)該如何實(shí)現(xiàn)羊精,所以JIT是跟具體虛擬機(jī)的實(shí)現(xiàn)有關(guān)的。本文以HotSpot虛擬機(jī)中的JIT為例說(shuō)明次屠。
解釋器和編譯器的對(duì)比
1)當(dāng)程序需要迅速啟動(dòng)和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用雳刺,省去編譯的時(shí)間劫灶,立即執(zhí)行。在程序運(yùn)行后掖桦,隨著時(shí)間推移本昏,編譯器逐漸發(fā)揮作用,把越來(lái)越多的代碼編譯成本地代碼枪汪,可以獲取更高的執(zhí)行效率涌穆。
2)當(dāng)程序運(yùn)行環(huán)境內(nèi)存資源限制較大怔昨,如:嵌入式系統(tǒng),可以使用解釋執(zhí)行節(jié)約內(nèi)存宿稀;反之使用編譯器執(zhí)行來(lái)提升效率趁舀。
3)解釋器可以作為編譯器激進(jìn)優(yōu)化時(shí)的一個(gè)【逃生門(mén)】,讓編譯器根據(jù)概率選擇一些大多數(shù)時(shí)候都能提升運(yùn)行速度的優(yōu)化手段祝沸,當(dāng)激進(jìn)優(yōu)化的假設(shè)不成立時(shí)矮烹,可以通過(guò)逆優(yōu)化退回到解釋狀態(tài)繼續(xù)執(zhí)行。【即編譯器失敗后罩锐,采用解釋器繼續(xù)執(zhí)行】
HotSpot虛擬機(jī)中內(nèi)置了兩個(gè)即時(shí)編譯器奉狈,C1【Client Compiler】和 C2【Server Compiler】。HotSpot會(huì)根據(jù)自身版本與宿主機(jī)器的硬件性能自動(dòng)選擇一個(gè)即時(shí)編譯器涩惑,并與解釋器配合工作仁期。
為了在程序響應(yīng)速度和運(yùn)行效率之間達(dá)到最佳平衡,HotSpot會(huì)逐漸啟用【分層編譯】策略竭恬,其中包括:
第0層:程序解釋執(zhí)行跛蛋,解釋器不開(kāi)啟性能監(jiān)控功能,可觸發(fā)第1層編譯萍聊。
第1層:也稱(chēng)為C1編譯问芬,將字節(jié)碼編譯為本地代碼,進(jìn)行簡(jiǎn)單寿桨、可靠的優(yōu)化此衅,如有必要將加入性能監(jiān)控的邏輯⊥っ【只關(guān)注局部?jī)?yōu)化挡鞍,放棄了耗時(shí)較長(zhǎng)的全局優(yōu)化】
第2層:也稱(chēng)為C2編譯,將字節(jié)碼編譯為本地代碼预烙,開(kāi)啟一些編譯較長(zhǎng)的優(yōu)化墨微,甚至?xí)鶕?jù)性能監(jiān)測(cè)信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
分層編譯后扁掸,C1【Client Compiler】和C2【Server Compiler】將同時(shí)工作翘县,許多代碼可能被多次編譯,用C1獲取更高的編譯速度谴分,用C2獲取更好的編譯質(zhì)量锈麸。
JIT的編譯對(duì)象
即時(shí)編譯的【熱點(diǎn)代碼】有兩類(lèi):
1)被多次調(diào)用的方法
2)被多次執(zhí)行的循環(huán)體
這兩種情況都是以整個(gè)方法作為編譯對(duì)象。對(duì)于情況2)而言牺蹄,JIT編譯發(fā)生在方法執(zhí)行過(guò)程之中忘伞,方法棧楨還在棧上,該方法就被替換為JIT編譯的產(chǎn)物,因此形象的稱(chēng)為【棧上替換氓奈,On Stack Replacement翘魄,OSR編譯】。
JIT觸發(fā)的條件
【多次執(zhí)行】中的多次是如何計(jì)算的舀奶,即如何判斷一段代碼是不是熱點(diǎn)代碼暑竟,需不需要即時(shí)編譯,這種行為稱(chēng)為【熱點(diǎn)探測(cè)】伪节。熱點(diǎn)探測(cè)的兩種方式:
1)基于采樣的熱點(diǎn)探測(cè)
虛擬機(jī)周期性的檢查各個(gè)線(xiàn)程的棧頂光羞,當(dāng)發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是熱點(diǎn)方法怀大。該方法能夠容易的獲取方法調(diào)用關(guān)系【將調(diào)用棧展開(kāi)即可】纱兑。
2)基于計(jì)數(shù)器的熱點(diǎn)探測(cè)【HostSpot采用】
虛擬機(jī)為每個(gè)方法建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù)化借,如果超過(guò)閾值則為熱點(diǎn)方法潜慎。
HotSpot使用第二種基于計(jì)數(shù)器的熱點(diǎn)探測(cè),每個(gè)方法中有兩類(lèi)計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器蓖康。閾值默認(rèn):在Client模式下為1500次铐炫,在Server模式下為10000次。
方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的不是方法被調(diào)用的絕對(duì)值蒜焊,因?yàn)楫?dāng)超過(guò)一定的時(shí)間限制倒信,如果方法的調(diào)用次數(shù)還是不能達(dá)到閾值,那這個(gè)方法計(jì)數(shù)器的值就會(huì)減半泳梆,這個(gè)過(guò)程稱(chēng)為【方法調(diào)用計(jì)數(shù)器熱度的衰減】鳖悠。
熱度衰減的動(dòng)作是在垃圾收集時(shí)順便進(jìn)行的∮琶睿可以通過(guò)參數(shù)關(guān)閉熱度衰減乘综,這樣方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的就是絕對(duì)值了。
回邊計(jì)數(shù)器:用于統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù)套硼。
在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱(chēng)為【回邊】卡辰。
統(tǒng)計(jì)的是絕對(duì)值,它沒(méi)有熱度衰減過(guò)程邪意。
還有其它熱點(diǎn)探測(cè)方法九妈,如:基于蹤跡的熱點(diǎn)探測(cè),Android中的Dalvik虛擬機(jī)就是使用該種方法雾鬼。
觸發(fā)JIT的過(guò)程
注意:當(dāng)兩個(gè)計(jì)數(shù)器之和超過(guò)閾值時(shí)萌朱,向編譯器提交JIT請(qǐng)求,這個(gè)時(shí)候不會(huì)等待JIT的結(jié)果呆贿,而是繼續(xù)按照解釋器的方式執(zhí)行字節(jié)碼嚷兔,直到JIT完成。當(dāng)JIT完成之后做入,這個(gè)方法的調(diào)用入口地址就被系統(tǒng)自動(dòng)改寫(xiě)成新的冒晰。即虛擬機(jī)在代碼編譯器還沒(méi)有完成之前,都將繼續(xù)按照解釋方式執(zhí)行竟块,而編譯動(dòng)作則在后臺(tái)編譯線(xiàn)程中進(jìn)行壶运。
JVM大部分優(yōu)化都是在JIT過(guò)程中,JIT產(chǎn)生的本地代碼比Javac產(chǎn)生的字節(jié)碼更加優(yōu)秀浪秘。
守護(hù)內(nèi)聯(lián) 內(nèi)聯(lián)緩存【激進(jìn)優(yōu)化】
內(nèi)聯(lián)優(yōu)化的好處:
1)去除方法調(diào)用的成本蒋情,如:建立棧楨。
2)方法內(nèi)聯(lián)后便可以在更大范圍上采取后續(xù)的優(yōu)化手段耸携。
對(duì)于使用invokespecial指令調(diào)用的私有方法棵癣、實(shí)例構(gòu)造器、父類(lèi)方法夺衍,以及使用invokestatic指令進(jìn)行調(diào)用的靜態(tài)方法狈谊,再有被final修飾的方法,他們?cè)诰幾g期間進(jìn)行解析沟沙『尤埃【但生成的class文件沒(méi)有做內(nèi)聯(lián)優(yōu)化】
-
虛方法的內(nèi)聯(lián)
為了解決虛方法的內(nèi)聯(lián)問(wèn)題,JVM引入了一種【類(lèi)型繼承關(guān)系分析矛紫,Class Hierarchy Analysis赎瞎,CHA】技術(shù),它用于確定在目標(biāo)已加載的類(lèi)中颊咬,某個(gè)接口是否有多于一種的實(shí)現(xiàn)务甥,某個(gè)類(lèi)是否存在子類(lèi)、子類(lèi)是否為抽象類(lèi)等信息贪染。
編譯器在進(jìn)行內(nèi)聯(lián)時(shí)缓呛,如果是非虛方法,則直接進(jìn)行內(nèi)聯(lián)杭隙,這個(gè)時(shí)候內(nèi)聯(lián)是有穩(wěn)定保障的哟绊。如果遇到虛方法,則會(huì)向CHA查詢(xún)此方法在當(dāng)前程序下是否有多個(gè)目標(biāo)版本痰憎。
if :如果只有一個(gè)版本票髓,則也可以進(jìn)行內(nèi)聯(lián),不過(guò)這種內(nèi)聯(lián)屬于激進(jìn)優(yōu)化铣耘,需要預(yù)留一個(gè)【逃生門(mén)】洽沟,稱(chēng)為【守護(hù)內(nèi)聯(lián)】。如果程序在后續(xù)執(zhí)行過(guò)程中蜗细,虛擬機(jī)一直沒(méi)有加載到會(huì)令這個(gè)方法的接收者的繼承關(guān)系發(fā)生變化的類(lèi)裆操,那這個(gè)內(nèi)聯(lián)優(yōu)化的代碼可以一直使用下去怒详。但如果加載了導(dǎo)致繼承關(guān)系發(fā)生變化的類(lèi),那就需要拋棄已經(jīng)編譯的代碼踪区,退回到解釋狀態(tài)執(zhí)行昆烁,或重新進(jìn)行編譯。
else :如果有多個(gè)版本的目標(biāo)方法可供選擇缎岗,此時(shí)編譯器使用【內(nèi)聯(lián)緩存】 來(lái)完成方法內(nèi)聯(lián)静尼,這是一個(gè)建立在目標(biāo)方法入口之前的緩存。
工作原理:在未發(fā)生方法調(diào)用之前传泊,內(nèi)聯(lián)緩存為空鼠渺;當(dāng)?shù)谝淮握{(diào)用發(fā)生后,內(nèi)聯(lián)緩存記錄下方法接收者的版本信息眷细,并且每次進(jìn)行方法調(diào)用時(shí)都比較接收者的版本拦盹,如果接收者的版本始終保持一致,那這個(gè)內(nèi)聯(lián)可以一直用下去溪椎;如果發(fā)生了方法接收者版本不一致的情況掌敬,說(shuō)明程序真正在使用虛方法的多態(tài)特性,此時(shí)取消內(nèi)聯(lián)池磁,查找【虛方法表】進(jìn)行方法分派奔害。
如此看來(lái),虛擬機(jī)都會(huì)把方法優(yōu)化為內(nèi)聯(lián)方法地熄,只不過(guò)在有些情況下為激進(jìn)優(yōu)化华临,當(dāng)激進(jìn)優(yōu)化被打破時(shí),再改為解釋狀態(tài)執(zhí)行端考,或重新編譯雅潭,或取消內(nèi)聯(lián)。
逃逸分析
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域却特。
方法逃逸:當(dāng)一個(gè)對(duì)象在方法中被定義后扶供,它被外部【方法】所引用,如:作為調(diào)用參數(shù)傳遞出去裂明。
線(xiàn)程逃逸:當(dāng)一個(gè)對(duì)象在方法中被定義后椿浓,它被外部【線(xiàn)程】所訪問(wèn),如:將該局部變量賦值給類(lèi)變量闽晦,或賦值給可以在其他線(xiàn)程中訪問(wèn)的實(shí)例變量扳碍。
對(duì)非逃逸對(duì)象可進(jìn)行的優(yōu)化:
1)棧上分配
當(dāng)一個(gè)對(duì)象不會(huì)逃逸出方法外,可以?xún)?yōu)化將對(duì)象內(nèi)存在棧上進(jìn)行分配仙蛉,這樣對(duì)象可以隨著棧楨出棧而銷(xiāo)毀笋敞,減小垃圾回收系統(tǒng)的壓力≤瘢【HotSpot不支持】
2)同步消除
當(dāng)變量不會(huì)逃逸出線(xiàn)程時(shí)夯巷,無(wú)法被其它線(xiàn)程訪問(wèn)到赛惩,可以將對(duì)這個(gè)變量實(shí)施的同步措施進(jìn)行消除。
3)標(biāo)量替換
標(biāo)量:數(shù)據(jù)無(wú)法再分解成更小的數(shù)據(jù)來(lái)表示趁餐,即Java中的基本數(shù)據(jù)類(lèi)型坊秸。
聚合量:數(shù)據(jù)可以再進(jìn)一步進(jìn)行分解,如:對(duì)象澎怒。
標(biāo)量替換:把一個(gè)Java對(duì)象拆分,將其使用到的成員變量恢復(fù)為原始類(lèi)型來(lái)訪問(wèn)阶牍。
如果一個(gè)對(duì)象不會(huì)逃逸喷面,那程序真正執(zhí)行的時(shí)候可能不創(chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的成員變量走孽。將對(duì)象拆分后惧辈,除了可以讓對(duì)象的成員變量在棧上分配和讀寫(xiě)之外,還可以為后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)造條件磕瓷。
標(biāo)量替換例子
package com.ljg;
public class T {
static class Person {
public String name;
public int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
public static void main(String[] args) {
Person person = new Person("ljg", 18);
System.out.println(person.name + "age is " + person.age);
}
}
上面代碼可能優(yōu)化為:【想不到的優(yōu)化】
public static void main(String[] args) {
String name = "ljg";
int age = 18;
System.out.println(name + "age is " + age);
}
這樣就不存在垃圾回收了盒齿。