一、前言
JVM運(yùn)行期的優(yōu)化主要是指程序在編譯成字節(jié)碼之后速挑,JVM通過解釋器去解釋執(zhí)行拘悦,再針對(duì)程序運(yùn)行的資源占用等情況進(jìn)行分析然后做出的一系列的優(yōu)化。Java程序的效率之所以較高(即使是和接近底層的c/c++語言相比較仆救,在Java內(nèi)部的即時(shí)編譯器優(yōu)化的情況下,很多應(yīng)用場(chǎng)景下效率也毫不遜色)矫渔,是離不開JVM對(duì)程序進(jìn)行的優(yōu)化的彤蔽,這篇博客就來總結(jié)一下虛擬機(jī)在背后給我們做的工作(針對(duì)的是目前市面上主流的HotSpot虛擬機(jī)而言)。
二庙洼、JVM的即時(shí)編譯器
1.解釋器與編譯器
JVM虛擬機(jī)采用的是解釋器與編譯器共存的架構(gòu)顿痪,這樣的搭配平均情況下能最好的發(fā)揮程序的性能。解釋器與編譯器各有優(yōu)勢(shì):當(dāng)程序需要迅速啟動(dòng)和快速執(zhí)行的時(shí)候油够,解釋器可以首先發(fā)揮作用员魏,省去編譯的時(shí)間,能夠立即執(zhí)行叠聋;而程序運(yùn)行一段時(shí)間后撕阎,即時(shí)編譯器(JIT)開始發(fā)揮作用,會(huì)根據(jù)程序代碼的運(yùn)行狀況碌补,把越來越多的代碼編譯成本地代碼虏束,以提高執(zhí)行的效率。當(dāng)程序在運(yùn)行環(huán)境中占用內(nèi)存資源較多時(shí)厦章,可以使用解釋器執(zhí)行來減少內(nèi)存的占用率镇匀。同時(shí),當(dāng)編譯器進(jìn)行過早優(yōu)化時(shí)袜啃,解釋器可以讓編譯器根據(jù)概率來選擇一些大多數(shù)情況下都能提高運(yùn)行速度的優(yōu)化手段汗侵,但如果這次優(yōu)化后,導(dǎo)致后續(xù)程序的運(yùn)行出現(xiàn)特殊狀況群发,這時(shí)即時(shí)編譯器又會(huì)通過逆優(yōu)化來回退到解釋器執(zhí)行晰韵。整個(gè)過程也就是下圖所示
從上圖也可以看出,編譯器里面內(nèi)置了Client和Server兩個(gè)編譯器(通常也被稱為c1和c2編譯器)熟妓,JVM采用了分層編譯的方式來使程序啟動(dòng)的響應(yīng)速度與運(yùn)行效率之間達(dá)到平衡:
1.第0層雪猪,程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控起愈,可觸發(fā)第一層編譯只恨。
2.第1層译仗,c1編譯,將字節(jié)碼編譯為本地代碼官觅,進(jìn)行簡(jiǎn)單可靠的優(yōu)化纵菌。
3.第2層,c2編譯休涤,將字節(jié)碼編譯為本地代碼咱圆,但是會(huì)進(jìn)行一些編譯耗時(shí)較長的優(yōu)化。
2.編譯對(duì)象與觸發(fā)條件
在JVM運(yùn)行過程中會(huì)被即使編譯器編譯的主要有被多次調(diào)用的方法和被多次執(zhí)行的循環(huán)體這些熱點(diǎn)代碼滑绒。這里需要注意,對(duì)于第一種情況隘膘,直接理所當(dāng)然的以整個(gè)方法作為JIT編譯的對(duì)象疑故,而對(duì)于后面一種情況,盡管編譯動(dòng)作是由循環(huán)體觸發(fā)的弯菊,編譯器還是會(huì)以它所在的整個(gè)方法為編譯對(duì)象纵势。這種編譯是發(fā)生在方法執(zhí)行的過程中,因此也被稱為棧上替換(OSR管钳,方法棧幀還在棧上钦铁,方法就被替換了)。
熱點(diǎn)代碼的判定方式
在現(xiàn)在的虛擬機(jī)中才漆,主要有以下幾種方式:
1.基于采樣的熱點(diǎn)探測(cè):采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程的棧頂牛曹,如果發(fā)現(xiàn)某個(gè)方法經(jīng)常出現(xiàn)在棧頂,這個(gè)方法就是熱點(diǎn)方法醇滥。這種方法實(shí)現(xiàn)起來較為簡(jiǎn)單黎比,可以很容易的獲取方法調(diào)用的關(guān)系,缺點(diǎn)是由于有線程阻塞或別的因素影響鸳玩,無法精確的對(duì)熱點(diǎn)進(jìn)行探測(cè)阅虫。
2.基于計(jì)數(shù)器的熱點(diǎn)探測(cè):采用這種方法的虛擬機(jī)會(huì)為每個(gè)方法(甚至是代碼塊)建立并維護(hù)計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù)不跟,執(zhí)行次數(shù)超過一定的閥值就會(huì)認(rèn)為它是熱點(diǎn)方法颓帝,這種方式更加精確和嚴(yán)謹(jǐn),但統(tǒng)計(jì)時(shí)需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器窝革,而且不能獲取方法的調(diào)用關(guān)系购城,實(shí)現(xiàn)起來較為麻煩。
3.基于蹤跡(Trace)的熱點(diǎn)探測(cè):采用這種方式的虛擬機(jī)是將一段頻繁執(zhí)行的代碼作為一個(gè)編譯單元虐译,并僅對(duì)該代碼片段進(jìn)行編譯工猜,該代碼片段由一個(gè)線性且連續(xù)的指令序列組成,僅有一個(gè)入口菱蔬,但有多個(gè)出口篷帅。也就是說史侣,基于蹤跡而編譯的熱點(diǎn)代碼不僅僅局限在一個(gè)單獨(dú)的方法或者代碼快中,一條Trace可能對(duì)應(yīng)多個(gè)方法魏身,代碼中頻繁執(zhí)行的路徑就可能被識(shí)別成不同的蹤跡惊橱。因此這種方法有著更高的精度,并且能夠避免編譯不是頻繁執(zhí)行的代碼箭昵,減少不必要的編譯開銷税朴,但這種方法的實(shí)現(xiàn)就更為的復(fù)雜。在Android早期的Dalvik虛擬機(jī)的JIT編譯器就是使用的這種方式(從Android4.4開始家制,Google就把Android中的Dalvik虛擬機(jī)無縫切換到了ART虛擬機(jī)正林,這里簡(jiǎn)單的說一下,Android上面的虛擬機(jī)是按照J(rèn)VM的部分規(guī)范去實(shí)現(xiàn)的一種類似的東西颤殴,并不屬于Java虛擬機(jī)觅廓,并且與JVM最大的不同是就是Dalvik/ART基于寄存器,而JVM基于棧涵但,Android里面是每個(gè)程序都對(duì)應(yīng)著一個(gè)單獨(dú)的虛擬機(jī)杈绸,也是一個(gè)單獨(dú)的進(jìn)程)。
而在JVM虛擬機(jī)中使用的是基于計(jì)數(shù)器的熱點(diǎn)探測(cè)矮瘟,至于為什么不使用基于蹤跡的熱點(diǎn)探測(cè)瞳脓,我想一個(gè)是實(shí)現(xiàn)上的困難,并且基于棧的JVM與寄存器的執(zhí)行速度無法相比澈侠,繁瑣的優(yōu)化反而會(huì)造成得不償失的局面劫侧。這種方式又包含了兩類計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器。
方法掉用計(jì)數(shù)器:統(tǒng)計(jì)一段時(shí)間內(nèi)方法被調(diào)用的次數(shù)哨啃,當(dāng)超過一個(gè)時(shí)間限度板辽,它的調(diào)用次數(shù)仍然不足以給JIT編器編譯,這個(gè)方法的調(diào)用計(jì)數(shù)久會(huì)減半棘催。
回邊計(jì)數(shù)器:主要是統(tǒng)計(jì)循環(huán)體內(nèi)的代碼執(zhí)行的次數(shù)劲弦,在字節(jié)碼遇到控制流后向后跳轉(zhuǎn)的指令稱為回邊,建立回邊計(jì)數(shù)器也是為了觸發(fā)OSR醇坝。因?yàn)橛行┣闆r下邑跪,比如空的循環(huán),照樣會(huì)執(zhí)行對(duì)應(yīng)的次數(shù)呼猪,但它是直接跳轉(zhuǎn)到自己画畅,所以JIT編譯器去編譯這種代碼是沒有任何意義的。
3.編譯過程
第一階段宋距,首先在字節(jié)碼層做一些系列的優(yōu)化轴踱,如方法內(nèi)聯(lián)、常量傳播等谚赎,然后將字節(jié)碼構(gòu)造成一種高級(jí)中間代碼(HIR)淫僻,HIR使用靜態(tài)單分配(根據(jù)調(diào)用的方法和它接收的參數(shù))的形式來代表代碼值诱篷,使得構(gòu)造HIR時(shí)的優(yōu)化更加容易實(shí)現(xiàn)。
第二階段雳灵,對(duì)傳來的HIR進(jìn)行空值檢查棕所、范圍檢查消除等優(yōu)化,然后從HIR中產(chǎn)生低級(jí)中間代碼(LIR)悯辙。
最后階段:使用線性掃描算法琳省,在LIR上分配寄存器,并在LIR上做窺孔優(yōu)化(局部的優(yōu)化方式躲撰,編譯器僅僅在一個(gè)或者多個(gè)基本塊中针贬,針對(duì)已經(jīng)生成的代碼,結(jié)合CPU自己指令的特點(diǎn)拢蛋,通過一些認(rèn)為可能帶來性能提升的轉(zhuǎn)換規(guī)則桦他,或者通過整體的分析,通過指令轉(zhuǎn)換瓤狐,提升代碼性能)瞬铸,然后產(chǎn)生機(jī)器代碼批幌。
整個(gè)大致的過程如下圖所示
三础锐、編譯優(yōu)化技術(shù)
JVM團(tuán)隊(duì)幾乎把所有代碼優(yōu)化的措施集中在了即時(shí)編譯器中,所以即時(shí)編譯器產(chǎn)生的本地代碼要比原來的解釋器解釋執(zhí)行字節(jié)碼所產(chǎn)生的本地代碼要更優(yōu)荧缘。優(yōu)化的技術(shù)非常的多皆警,這里就不做過多的描述了。重點(diǎn)整理一下JVM中比較前沿的逃逸分析截粗。
逃逸分析并不是直接優(yōu)化代碼的手段信姓,而是為其它優(yōu)化方式提供的分析技術(shù)。逃逸分析的基本行為就是分析對(duì)象的動(dòng)態(tài)作用域(當(dāng)一個(gè)對(duì)象在方法中被定義之后绸罗,可能被外部方法所引用意推,例如作為參數(shù)傳遞到其他方法中,稱為方法逃逸珊蟀;可能被外部線程訪問到菊值,比如賦值給類變量或可以在其它線程中訪問的實(shí)例變量,稱為線程逃逸)育灸。如果別的方法或線程無法通過任何途徑訪問到這個(gè)對(duì)象腻窒,就能為這個(gè)對(duì)象下列高效的優(yōu)化。
1.棧上分配
在JVM中磅崭,創(chuàng)建對(duì)象所需的內(nèi)存是從Java堆上分配出來的儿子,而Java堆中的對(duì)象對(duì)各個(gè)線程都是共享和可見的,只要持有這個(gè)對(duì)象的引用砸喻,就能獲取Java堆中所儲(chǔ)存的該對(duì)象的數(shù)據(jù)柔逼,JVM中的垃圾收集器可以回收堆中不再使用的對(duì)象蒋譬,但是篩選可回收對(duì)象以及回收和整理內(nèi)存都需要比較大的時(shí)間開銷。如果能確定一個(gè)對(duì)象不會(huì)出現(xiàn)方法逃逸的情況卒落,就可以直接在棧上分配對(duì)象的內(nèi)存羡铲,對(duì)象隨著棧幀出棧而消耗,這樣一來可以很大程度的減少垃圾收集系統(tǒng)的壓力儡毕。
2.同步消除
如果確定一個(gè)對(duì)象不會(huì)出現(xiàn)線程逃逸也切,也就是不會(huì)被其它線程訪問到,那么對(duì)這個(gè)對(duì)象的的讀寫操作肯定不會(huì)出現(xiàn)競(jìng)爭(zhēng)腰湾,就可以直接去除同步的措施雷恃,線程的同步操作也是一個(gè)比較耗時(shí)的操作,
3.標(biāo)量替換
標(biāo)量是指一個(gè)數(shù)據(jù)無法被分解成更小的數(shù)據(jù)來表示了费坊,例如Java中的原始數(shù)據(jù)類型就是這樣倒槐。相反,聚合量就表示一個(gè)能繼續(xù)分解的數(shù)據(jù)附井,Java中的對(duì)象就是如此讨越。所以如果逃逸分析證明了一個(gè)對(duì)象不會(huì)被外部訪問,那么就可以根據(jù)程序訪問的情況永毅,把對(duì)象拆分把跨,然后把里面用到的成員變量替換成原始類型,這種方式就被稱為標(biāo)量替換沼死。因?yàn)闃?biāo)量也是直接存儲(chǔ)在棧上面的着逐,而棧上儲(chǔ)存的數(shù)據(jù)有大概率會(huì)被分配到CPU的寄存器上,并且存儲(chǔ)到棧上也方便后續(xù)的優(yōu)化操作意蛀。
逃逸分析也是一種比較重要的間接優(yōu)化手段耸别,但是由于逃逸分析也會(huì)有過多的消耗,和前面提到的基于蹤跡的熱點(diǎn)代碼探測(cè)的手段一樣县钥,可能會(huì)獲得的利益小于自身的消耗秀姐,所以虛擬機(jī)也去權(quán)衡是否需要完全準(zhǔn)確的逃逸分析。
四若贮、總結(jié)
主體內(nèi)容還是來自《深入理解Java虛擬機(jī)》省有,也穿插了書中沒有細(xì)講的一些內(nèi)容,這篇博客主要內(nèi)容雖然是總結(jié)的JVM運(yùn)行期的優(yōu)化兜看,但其實(shí)基本上都是在描述JIT編譯器在背后所做的工作锥咸,了解這些能對(duì)Java為何會(huì)如此高效有一個(gè)大體的認(rèn)識(shí),也能增加對(duì)寫出的代碼更加深層次的思考细移,這篇博客就先到這里了搏予。
參考:
https://blog.csdn.net/Luoshengyang/article/details/18006645
http://www.shcas.net/jsjyup/pdf/2017/3/%E5%9F%BA%E4%BA%8ETrace%E7%9A%84CMinus%E8%AF%AD%E8%A8%80%E5%8D%B3%E6%97%B6%E7%BC%96%E8%AF%91%E6%8A%80%E6%9C%AF.pdf