前言
前面介紹過(guò)
JVM被分為三個(gè)主要的子系統(tǒng):
- 類加載器子系統(tǒng)
- 運(yùn)行時(shí)數(shù)據(jù)區(qū)(也就是內(nèi)存相關(guān))
執(zhí)行引擎
前幾章我們簡(jiǎn)單的梳理了一下JVM的類加載機(jī)制及運(yùn)行時(shí)數(shù)據(jù)區(qū),
今天我們來(lái)聊聊JVM執(zhí)行引擎.
如無(wú)特殊說(shuō)明, 所有描述JVM的特性均特指HotSpot VM
Java程序的執(zhí)行過(guò)程
簡(jiǎn)單回顧下Java程序的執(zhí)行過(guò)程
-
編譯 (Java前端編譯器)
將Java文件編譯為.class字節(jié)碼文件, 這部分工作由Java前端編譯器完成, 與JVM本身其實(shí)沒(méi)什么關(guān)系;
編譯過(guò)程如下圖
編譯 加載(JVM-類加載器)
負(fù)責(zé)"裝載字節(jié)碼", 但字節(jié)碼并不能夠直接運(yùn)行在操作系統(tǒng)之上, 因?yàn)樽止?jié)碼指令并非等價(jià)于本地機(jī)器指令, 它內(nèi)部包含的僅僅只是一些能夠被JVM鎖識(shí)別的字節(jié)碼指令、符號(hào)表和其他輔助信息.運(yùn)行(JVM-執(zhí)行引擎)
Java字節(jié)碼的執(zhí)行是由JVM執(zhí)行引擎來(lái)完成
執(zhí)行引擎
執(zhí)行引擎的任務(wù)就是將字節(jié)碼指令解釋/編譯為對(duì)應(yīng)平臺(tái)上的本地機(jī)器指令.
簡(jiǎn)單來(lái)說(shuō),JVM中的執(zhí)行引擎充當(dāng)了將高級(jí)語(yǔ)言翻譯為機(jī)器語(yǔ)言的翻譯官.
科普(翻譯官)
正式介紹執(zhí)行引擎前, 我們先了解下現(xiàn)實(shí)世界的翻譯官這個(gè)職業(yè).
看過(guò)一些跨國(guó)演講節(jié)目的都知道, 一般都會(huì)配多個(gè)翻譯官,來(lái)實(shí)現(xiàn)語(yǔ)言互通.
細(xì)心的朋友可能會(huì)發(fā)現(xiàn), 翻譯的形式一般有兩種
交替?zhèn)髯g
領(lǐng)導(dǎo)講一句, 翻譯官翻譯一句, 領(lǐng)導(dǎo)繼續(xù)講, 如此往復(fù)同聲傳譯
領(lǐng)導(dǎo)講的同時(shí), 翻譯官也同時(shí)翻譯, 基本上領(lǐng)導(dǎo)講完, 翻譯也完成了
不難看出, 同聲傳譯是實(shí)效性比較高的方式, 同時(shí)對(duì)翻譯官的能力、壓力也是極大的.
而交替?zhèn)髯g則顯得從容了很多, 不過(guò)實(shí)效性就差了很多.
那么, 有沒(méi)有辦法可以讓同聲傳譯的門(mén)檻降低點(diǎn)呢? 讓普通點(diǎn)的翻譯官都能勝任這樣的工作呢?!
聰明的小伙伴肯定想到了:
如果能夠提前知道大佬演講的內(nèi)容, 提前翻譯好個(gè)大概, 不就完美解決這個(gè)問(wèn)題了么?!
理想情況下, 是外國(guó)人照著稿子讀, 翻譯官按著提前準(zhǔn)備好的翻譯稿讀出來(lái).
實(shí)際情況下, 這種照本宣科的場(chǎng)景還是比較少的, 大部分情況下是演講者會(huì)有個(gè)大綱, 翻譯官也會(huì)提前拿到, 然后真正演講時(shí)外國(guó)人順著大綱穿插自由發(fā)揮, 這樣, 翻譯官需要注意的其實(shí)就是自由發(fā)揮的那部分內(nèi)容, 這將大大的減輕了翻譯的壓力.
PS: 更真實(shí)通用的情況是:
翻譯官憑借自身的老練和職場(chǎng)經(jīng)驗(yàn), 能夠熟悉的掌握一些話術(shù)的翻譯, 這些話術(shù)來(lái)自于多年的工作經(jīng)驗(yàn).
比如正式的演講“尊敬的各位.....”, 這些其實(shí)是通用的標(biāo)準(zhǔn)的東西, 翻譯官完全可以提前準(zhǔn)備好;
或者, 翻譯官和演講者配合多年, 很有默契, 完全掌握演講者的演講習(xí)慣、話術(shù), 這種默契將有助于翻譯官提前預(yù)知演講者接下來(lái)的發(fā)言.
言歸正傳, 我們開(kāi)始了解執(zhí)行引擎這個(gè)“字節(jié)碼翻譯官”的翻譯方式.
- 字節(jié)碼解釋器 (交替?zhèn)髯g)
當(dāng)Java虛擬機(jī)啟動(dòng)時(shí)會(huì)根據(jù)預(yù)定義的規(guī)范對(duì)字節(jié)碼采用逐行編譯的方式執(zhí)行,將每條字節(jié)碼文件中的內(nèi)容“翻譯”為對(duì)應(yīng)平臺(tái)的本地機(jī)器指令并執(zhí)行.
- JIT編譯器 (同聲傳譯)
JVM將源碼直接編譯為和本地機(jī)器平臺(tái)相關(guān)的機(jī)器語(yǔ)言,但是并不會(huì)立刻執(zhí)行.
JIT編譯器在運(yùn)行時(shí)會(huì)針對(duì)那些頻繁被調(diào)用的“熱點(diǎn)代碼”做出深度優(yōu)化,將其直接編譯為對(duì)應(yīng)平臺(tái)的本地機(jī)器指令,以此提升Java程序的執(zhí)行性能.即時(shí)編譯的目的是避免函數(shù)被解釋執(zhí)行, 而是將整個(gè)函數(shù)體編譯成為機(jī)器碼,每次函數(shù)執(zhí)行時(shí),只執(zhí)行編譯后的機(jī)器碼即可,這種方式可以使執(zhí)行效率大幅度提升.
簡(jiǎn)單的看完以上兩種方式, 不難看出, 其實(shí)JVM執(zhí)行引擎這個(gè)“翻譯官”的翻譯方式和現(xiàn)實(shí)中的翻譯官?zèng)]什么區(qū)別.
早先, JVM執(zhí)行引擎其實(shí)只有“字節(jié)碼解釋器 ”這一種方式, 效率低下, 和C、C++這些語(yǔ)言的執(zhí)行效率簡(jiǎn)直無(wú)法相比;
直到后來(lái)引入了JIT編譯器, 一方面將一些函數(shù)、固定的代碼提前編譯好;
另一方面針對(duì)一些不固定的則繼續(xù)保持解釋執(zhí)行的方式; JVM把兩者搭配使用, 極大的提升了整體的效率.
熱點(diǎn)代碼及探測(cè)方式
JVM執(zhí)行引擎是采用了解釋執(zhí)行+編譯執(zhí)行的混合方式.
PS: 這也就是我們傻傻分不清楚JAVA究竟是解釋型語(yǔ)言還是編譯型語(yǔ)言的原因.
同時(shí)我們也提到了執(zhí)行引擎中的JIT即時(shí)編譯, 關(guān)于JIT即時(shí)編譯有個(gè)很重要的概念, 即熱點(diǎn)代碼.
JIT只會(huì)對(duì)熱點(diǎn)代碼進(jìn)行編譯
熱點(diǎn)代碼
一個(gè)被多次調(diào)用的方法,或者是一個(gè)方法體內(nèi)部循環(huán)次數(shù)較多的循環(huán)體都可以被稱之為“熱點(diǎn)代碼”.
因此都可以通過(guò)JIT編譯器編譯為本地機(jī)器指令.
由于這種編譯方式發(fā)生在方法的執(zhí)行過(guò)程中,因此也被稱之為棧上替換,或簡(jiǎn)稱為OSR (On StackReplacement)編譯.PS: 熱點(diǎn)代碼經(jīng)過(guò)JIT即時(shí)編譯后成為機(jī)器指令,需要緩存起來(lái)Code Cache,存放在方法區(qū)(元空間/本地內(nèi)存)
一個(gè)方法究竟要被調(diào)用多少次,或者一個(gè)循環(huán)體究竟需要執(zhí)行多少次循環(huán)才可以達(dá)到這個(gè)標(biāo)準(zhǔn)?
必然需要一個(gè)明確的閾值,JIT編譯器才會(huì)將這些“熱點(diǎn)代碼”編譯為本地機(jī)器指令執(zhí)行.
這里主要依靠熱點(diǎn)探測(cè)功能.
熱點(diǎn)探測(cè)
目前HotSpot VM所采用的熱點(diǎn)探測(cè)方式是基于計(jì)數(shù)器的熱點(diǎn)探測(cè).
采用基于計(jì)數(shù)器的熱點(diǎn)探測(cè),HotSpot VM將會(huì)為每一個(gè) 方法都建立2個(gè)不同類型的計(jì)數(shù)器,分別為
- 方法調(diào)用計(jì)數(shù)器(Invocation Counter)
- 回邊計(jì)數(shù)器(BackEdge Counter).
方法調(diào)用計(jì)數(shù)器用于統(tǒng)計(jì)方法的調(diào)用次數(shù), 回邊計(jì)數(shù)器則用于統(tǒng)計(jì)循環(huán)體執(zhí)行的循環(huán)次數(shù).
- 方法調(diào)用計(jì)數(shù)器
這個(gè)計(jì)數(shù)器就用于統(tǒng)計(jì)方法被調(diào)用的次數(shù),它的默認(rèn)閾值在Client 模式下是1500 次, 在Server 模式下是10000 次.
超過(guò)這個(gè)閾值,就會(huì)觸發(fā)JIT編譯.
這個(gè)閾值可以通過(guò)虛擬機(jī)參數(shù)來(lái)人為設(shè)定.-XX :CompileThreshold
當(dāng)一個(gè)方法被調(diào)用時(shí),會(huì)先檢查該方法是否存在被JIT編譯過(guò)的版本,
如果存在,則優(yōu)先使用編譯后的本地代碼來(lái)執(zhí)行.
如果不存在已被編譯過(guò)的版本,則將此方法的調(diào)用計(jì)數(shù)器值加1,然后判斷方法調(diào)用計(jì)數(shù)器與回邊計(jì)>數(shù)器值之和是否超過(guò)方法調(diào)用計(jì)數(shù)器的閾值.
如果已超過(guò)閾值,那么將會(huì)向即時(shí)編譯器提交一個(gè)該方法的代碼編譯請(qǐng)求.
- 回邊計(jì)數(shù)器
它的作用是統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令稱為“回邊” (Back Edge).
顯然,建立回邊計(jì)數(shù)器統(tǒng)計(jì)的目的就是為了觸發(fā)OSR編譯.
熱度衰減
如果不做任何設(shè)置,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的并不是方法被調(diào)用的絕對(duì)次數(shù),而是一個(gè)相對(duì)的執(zhí)行頻率,即一段時(shí)間之內(nèi)方法被調(diào)用的次數(shù).
當(dāng)超過(guò)一定的時(shí)間限度,如果方法的調(diào)用次數(shù)仍然不足以讓它提交給即時(shí)編譯器編譯,那這個(gè)方法的調(diào)用計(jì)數(shù)器就會(huì)被減少一半,這個(gè)過(guò)程稱為方法調(diào)用計(jì)數(shù)器熱度的衰減(Counter Decay),而這段時(shí)間就稱為此方法統(tǒng)計(jì)的半衰周期(Counter Half Life Time);
進(jìn)行熱度衰減的動(dòng)作是在虛擬機(jī)進(jìn)行垃圾收集時(shí)順便進(jìn)行的,
可以使用虛擬機(jī)參數(shù) -XX:-UseCounterDecay來(lái)關(guān)閉熱度衰減,讓方法計(jì)數(shù)器統(tǒng)計(jì)方法調(diào)用的絕對(duì)次數(shù);這樣,只要系統(tǒng)運(yùn)行時(shí)間足夠長(zhǎng),絕大部分方法都會(huì)被編譯成本地代碼.
另外,可以使用-XX:CounterHalfLifeTime參數(shù)設(shè)置半衰周期的時(shí)間,單位是秒.
其他熱點(diǎn)探測(cè)技術(shù)
HotSpot中的熱點(diǎn)代碼探測(cè)是基于計(jì)數(shù)器模式實(shí)現(xiàn)的, 但是除了計(jì)數(shù)器的方式探測(cè)之外, 還可以基于采樣(sampling)以及蹤跡(Trace)模式對(duì)代碼進(jìn)行熱點(diǎn)探測(cè).
采樣探測(cè)
采用這種探測(cè)技術(shù)的虛擬機(jī)會(huì)周期性的檢查每個(gè)線程的虛擬機(jī)棧棧頂,如果一些在檢查時(shí)經(jīng)常出現(xiàn)在棧頂?shù)姆椒?那么就代表這個(gè)方法經(jīng)常被調(diào)用執(zhí)行,對(duì)于這類方法可以判定為熱點(diǎn)方法.
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,可以很輕松的判定出熱度很高(調(diào)用次數(shù)頻繁)的方法
- 缺點(diǎn):無(wú)法實(shí)現(xiàn)精準(zhǔn)探測(cè),因?yàn)闄z查是周期性的,并且有些方法中存在線程阻塞、休眠等因素,會(huì)導(dǎo)致有些方法無(wú)法被精準(zhǔn)檢測(cè)
蹤跡探測(cè)
采用這種方式的虛擬機(jī)是將一段頻繁執(zhí)行的代碼作為一個(gè)編譯單元,并僅對(duì)該單元進(jìn)行編譯,該單元由一個(gè)線性且連續(xù)的指令集組成,僅有一個(gè)入口,但有多個(gè)出口.
也就代表著:基于蹤跡而編譯的熱點(diǎn)代碼不僅僅局限在一個(gè)單獨(dú)的方法或者代碼塊中,一條蹤跡可能對(duì)應(yīng)多個(gè)方法,代碼中頻繁執(zhí)行的路徑就可能被識(shí)別成不同的蹤跡.
- 優(yōu)點(diǎn):這種方式實(shí)現(xiàn)可以使得熱點(diǎn)探測(cè)擁有更高精度,可以避免將一塊代碼塊中所有的代碼都進(jìn)行編譯的情況出現(xiàn),能夠在很大程序上減少不必要的編譯開(kāi)銷.
因?yàn)闊o(wú)論是采樣探測(cè)還是計(jì)數(shù)器探測(cè)的方式,都是以方法體或循環(huán)體作為編譯的基本單元的. - 缺點(diǎn):蹤跡探測(cè)的實(shí)現(xiàn)過(guò)程非常復(fù)雜,難度非常高.
HotSpot虛擬機(jī)采用的計(jì)數(shù)探測(cè)的方式,實(shí)現(xiàn)難度、編譯開(kāi)銷與探測(cè)精準(zhǔn)三者之間會(huì)有一個(gè)很好的權(quán)衡.
三種探測(cè)技術(shù)比較如下:
- 實(shí)現(xiàn)難度:采樣探測(cè) < 計(jì)數(shù)探測(cè) < 蹤跡探測(cè)
- 探測(cè)精度:采樣探測(cè) < 計(jì)數(shù)探測(cè) < 蹤跡探測(cè)
- 編譯開(kāi)銷:蹤跡探測(cè) < 計(jì)數(shù)探測(cè) < 采樣探測(cè)
JVM為何不移除解釋器?
很多同學(xué)可能會(huì)有疑惑, 如果以純JIT編譯器的方式執(zhí)行,性能方面絕對(duì)會(huì)超出解釋器+編譯器混合的模式,但為何虛擬機(jī)至今也不移除解釋器,還要用解釋器來(lái)拖累Java程序的性能呢?
其實(shí)主要有兩個(gè)原因:
- 保證Java的絕對(duì)跨平臺(tái)性
如果將解釋器從虛擬機(jī)中移除就代表著:
每到一個(gè)不同的平臺(tái),比如從Windows遷移到Linux環(huán)境,那么JIT又要重新編譯生成對(duì)應(yīng)平臺(tái)的機(jī)器碼指令才能讓Java程序執(zhí)行
- 保證啟動(dòng)速度
如果移除了解釋器模塊,那么就代表著所有的字節(jié)碼指令需要在啟動(dòng)時(shí)全部先編譯為本地的機(jī)械碼,這樣才能使得Java程序能夠正常執(zhí)行.
此時(shí)時(shí)間開(kāi)銷是巨大的,那么會(huì)導(dǎo)致一些需要緊急上線的項(xiàng)目可能編譯都需要等半天的時(shí)間
PS: 的確有JVM的實(shí)現(xiàn)移除了解釋器模塊, 就是號(hào)稱“史上最快”的JRockitVM
總結(jié)
HotSpot中采用的是解釋器+JIT即時(shí)編譯器混合.
好處
在Java程序運(yùn)行時(shí),JVM可以快速啟動(dòng),前期先由解釋器發(fā)揮作用,不需要等到編譯器把所有字節(jié)碼指令編譯完之后才執(zhí)行,這樣可以省去很大一部分的編譯時(shí)間;
后續(xù)隨著程序在線上運(yùn)行的時(shí)間越來(lái)越久,JIT發(fā)揮作用,慢慢的將一些程序中的熱點(diǎn)代碼替換為本地機(jī)器碼運(yùn)行,這樣可以讓程序的執(zhí)行效率更高.
同時(shí),因?yàn)镠otSpotVM中存在熱度衰減的概念,所以當(dāng)一段代碼的熱度下降時(shí),JIT會(huì)取消對(duì)它的編譯,重新更換為解釋器執(zhí)行的模式工作, HotSpot的這種執(zhí)行模式也被成為“自適應(yīng)優(yōu)化”執(zhí)行.
當(dāng)然,我們?cè)诔绦騿?dòng)時(shí)也可以通過(guò)JVM參數(shù)自己指定執(zhí)行模式
- -Xint:完全采用解釋器模式執(zhí)行程序
- -Xcomp:完全采用即時(shí)編譯器模式執(zhí)行程序. 如果即時(shí)編譯器出現(xiàn)問(wèn)題,解釋器會(huì)介入執(zhí)行
- -Xmixed:采用解釋器+JIT即時(shí)編譯器的混合模式共同執(zhí)行(默認(rèn)的執(zhí)行方式)
HotSpot VM 中的JIT分類
在HotSpot VM中內(nèi)嵌有兩個(gè)JIT編譯器
Client Compiler(簡(jiǎn)稱C1)
Server Compiler(簡(jiǎn)稱C2)
C1編譯器(Client Compiler)
C1編譯器主要追求穩(wěn)定和編譯速度,屬于保守派.
C1中常見(jiàn)的優(yōu)化方案有幾種
- 公共子表達(dá)式消除
如果一個(gè)表達(dá)式E已經(jīng)計(jì)算過(guò)了,并且從先前的計(jì)算到現(xiàn)在E中所有變量的值都沒(méi)有發(fā)生變化,那E的這次出現(xiàn)就成公共子表達(dá)式,可以用原先的表達(dá)式進(jìn)行消除,直接使用上次的計(jì)算結(jié)果,無(wú)需再次計(jì)算
- 方法內(nèi)聯(lián)
將引用的方法代碼編譯到引用點(diǎn)處,這樣可以減少棧幀的生成,減少參數(shù)傳遞以及跳轉(zhuǎn)過(guò)程
- 去虛擬化
對(duì)唯一的實(shí)現(xiàn)類進(jìn)行內(nèi)聯(lián)
- 冗余消除
通過(guò)對(duì)字節(jié)碼指令進(jìn)行流分析,將一些運(yùn)行過(guò)程中不會(huì)執(zhí)行的代碼消除
- 空檢測(cè)消除
將顯式調(diào)用的NullCheck(空指針判斷)擦除,改成ImplicitNullCheck異常信號(hào)機(jī)制處理- 自動(dòng)裝箱消除
對(duì)于一些不必要的裝箱操作會(huì)被消除,比如剛裝箱的數(shù)據(jù)又在后面立馬被拆箱,這種無(wú)用操作就會(huì)被消除- 安全點(diǎn)消除
對(duì)于線程無(wú)法抵達(dá)或不會(huì)停留的安全點(diǎn)會(huì)進(jìn)行消除- 反射消除
對(duì)于一些可以正常訪問(wèn)無(wú)需通過(guò)反射機(jī)制獲取的數(shù)據(jù),會(huì)被改為直接訪問(wèn),消除反射操作
C2編譯器(Server Compiler)
C2編譯器則主要是追求編譯后的執(zhí)行性能,屬于激進(jìn)派.
C2編譯器建立在C1編譯器的基礎(chǔ)優(yōu)化之上,除了使用C1中的優(yōu)化手段之外,還有幾種基于逃逸分析的激進(jìn)優(yōu)化手段
- 標(biāo)量替換
用標(biāo)量值代替聚合對(duì)象的屬性值
- 棧上分配
對(duì)于未逃逸的對(duì)象分配對(duì)象在棧而不是堆
- 同步消除
清除同步操作,通常指synchronized
逃逸分析
Java 中對(duì)象的創(chuàng)建一般會(huì)由堆內(nèi)存去分配內(nèi)存空間來(lái)進(jìn)行存儲(chǔ),在堆內(nèi)存空間不足的時(shí)候,GC 便會(huì)對(duì)堆內(nèi)存進(jìn)行垃圾回收.
如果 GC 運(yùn)行的次數(shù)過(guò)多,便會(huì)影響程序的性能,所以 “逃逸分析” 由此誕生.
它的目的就是判斷哪些對(duì)象是可以存儲(chǔ)在棧內(nèi)存中而不用存儲(chǔ)在堆內(nèi)存中,從而讓其隨著線程的消逝而消逝,進(jìn)而減少 GC 發(fā)生的頻率.
簡(jiǎn)而言之, 逃逸分析就是Hotspot 虛擬機(jī)可以分析新創(chuàng)建對(duì)象的使用范圍,并決定是否在 Java 堆上分配內(nèi)存的一項(xiàng)技術(shù).
逃逸分析是建立在方法為單位之上的,如果一個(gè)成員對(duì)象在方法體中產(chǎn)生,但是直至方法結(jié)束也沒(méi)有走出方法體的作用域, 那么該成員就可以被理解為未逃逸.
反之,如果一個(gè)成員在方法最后被“return”出去了, 或在方法體的邏輯中被賦值給了外部成員,那么則代表著該成員逃逸了.
逃逸的方式
- 方法逃逸
一個(gè)對(duì)象在方法中被定義,但卻被方法以外的其他代碼使用.
在一個(gè)方法體內(nèi),定義一個(gè)局部變量,而它可能被外部方法引用.
比如作為調(diào)用參數(shù)傳遞給方法,或作為對(duì)象直接返回
可以理解為對(duì)象跳出了方法
- 線程逃逸
一個(gè)對(duì)象由某個(gè)線程在方法中被定義,但卻被其他線程訪問(wèn)
這個(gè)對(duì)象被其他線程訪問(wèn)到,比如賦值給了實(shí)例變量,并被其他線程訪問(wèn)到了.
可以理解為對(duì)象逃出了當(dāng)前線程
逃逸的狀態(tài)
一個(gè)對(duì)象有三種逃逸狀態(tài)
- 全局逃逸
一個(gè)對(duì)象的作用范圍逃出了當(dāng)前方法或者當(dāng)前線程
一般有以下幾種場(chǎng)景
- 對(duì)象是一個(gè)靜態(tài)變量
- 對(duì)象是一個(gè)已經(jīng)發(fā)生逃逸的對(duì)象
- 對(duì)象作為當(dāng)前方法的返回值
- 參數(shù)逃逸
一個(gè)對(duì)象被作為方法參數(shù)傳遞或者被參數(shù)引用,但在調(diào)用過(guò)程中不會(huì)發(fā)生全局逃逸,這個(gè)狀態(tài)是通過(guò)被調(diào)方法的字節(jié)碼確定的
- 沒(méi)有逃逸
方法中的對(duì)象沒(méi)有發(fā)生逃逸
逃逸分析的好處
逃逸分析的作用,就是篩選出沒(méi)有發(fā)生逃逸的對(duì)象,從而對(duì)它們進(jìn)行以下三方面的優(yōu)化
- 同步消除(鎖消除)
如果你定義的類的方法上有同步鎖,但在運(yùn)行時(shí),卻只有一個(gè)線程在訪問(wèn),此時(shí)逃逸分析后的機(jī)器碼,會(huì)去掉同步鎖運(yùn)行
標(biāo)量替換
Java虛擬機(jī)中的原始數(shù)據(jù)類型(int,long等數(shù)值類型以及reference類型等)都不能再進(jìn)一步分解,它們可以稱為標(biāo)量.
相對(duì)的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它稱為聚合量.
Java中最典型的聚合量是對(duì)象.
如果逃逸分析證明一個(gè)對(duì)象不會(huì)被外部訪問(wèn),并且這個(gè)對(duì)象是可分解的,那程序真正執(zhí)行的時(shí)候?qū)⒖赡懿粍?chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來(lái)代替.
拆散后的變量便可以被單獨(dú)分析與優(yōu)化,可以各自分別在棧幀或寄存器上分配空間,原本的對(duì)象就無(wú)需整體分配空間了棧內(nèi)存分配
將原本分配在堆內(nèi)存上的對(duì)象轉(zhuǎn)而分配在棧內(nèi)存上,這樣就可以減少堆內(nèi)存的占用,從而減少 GC 的頻次
相關(guān)參數(shù)配置
-XX:+DoEscapeAnalysis 開(kāi)啟逃逸分析(JDK1.8 中是默認(rèn)開(kāi)啟)
-XX:-DoEscapeAnalysis 關(guān)閉逃逸分析
-XX:+PrintEscapeAnalysis 顯示分析結(jié)果
-XX:+EliminateLocks 開(kāi)啟鎖消除(JDK1.8 中是默認(rèn)開(kāi)啟)
-XX:-EliminateLocks 關(guān)閉鎖消除
-XX:+EliminateAllocations 開(kāi)啟標(biāo)量替換(JDK1.8 中是默認(rèn)開(kāi)啟)
-XX:-EliminateAllocations 關(guān)閉標(biāo)量替換
-XX:+PrintEliminateAllocations 顯示標(biāo)量替換詳情
逃逸實(shí)例
下面這段代碼演示了逃逸
public class EscapeAnalysisDemo {
public static Object globalVariableObject;
public Object instanceObject;
public void globalVariableEscape(){
globalVariableObject = new Object(); // 靜態(tài)變量,外部線程可見(jiàn),發(fā)生逃逸
}
public void instanceObjectEscape(){
instanceObject = new Object(); // 賦值給堆中實(shí)例字段,外部線程可見(jiàn),發(fā)生逃逸
}
public Object returnObjectEscape(){
return new Object(); // 返回實(shí)例,外部線程可見(jiàn),發(fā)生逃逸
}
public void noEscape(){
Object noEscape = new Object(); // 僅創(chuàng)建線程可見(jiàn),對(duì)象無(wú)逃逸
}
}
C1和C2的對(duì)比
- C2編譯器啟動(dòng)時(shí)長(zhǎng)比C1編譯器慢
- 系統(tǒng)穩(wěn)定執(zhí)行以后C2編譯器執(zhí)行速度遠(yuǎn)遠(yuǎn)快于C1編譯器
程序解釋執(zhí)行(不開(kāi)啟性能監(jiān)控)可以觸發(fā)C1編譯,將字節(jié)碼編譯成機(jī)器碼,可以進(jìn)行簡(jiǎn)單優(yōu)化,也可以加上性能監(jiān)控;
C2編譯會(huì)根據(jù)性能監(jiān)控信息進(jìn)行激進(jìn)優(yōu)化.
不過(guò)在Java7版本之后,一旦開(kāi)發(fā)人員在程序中顯式指定命令“-server"時(shí),默認(rèn)將會(huì)開(kāi)啟分層編譯策略,由C1編譯器和C2編譯器相互協(xié)作共同來(lái)執(zhí)行編譯任務(wù).
開(kāi)發(fā)人員可以通過(guò)命令顯式指定Java虛擬機(jī)在運(yùn)行時(shí)到底使用哪一種即時(shí)編譯器
-client
指定Java虛擬機(jī)運(yùn)行在Client模式下,并使用C1編譯器;
C1編譯器會(huì)對(duì)字節(jié)碼進(jìn)行簡(jiǎn)單和可靠的優(yōu)化,耗時(shí)短,以達(dá)到更快的編譯速度
-server
指定Java虛擬機(jī)運(yùn)行在Server模式下,并使用C2編譯器.
C2進(jìn)行耗時(shí)較長(zhǎng)的優(yōu)化,以及激進(jìn)優(yōu)化,但優(yōu)化的代碼執(zhí)行效率更高
注意:64位操作系統(tǒng)默認(rèn)使用-server服務(wù)器模式,即C2編譯器
Graal編譯器
JDK10起,HotSpot又加入一個(gè)全新的即時(shí)編譯器: Graal編譯器
編譯效果短短幾年時(shí)間就追平了C2編譯器,未來(lái)可期.
目前,帶著“實(shí)驗(yàn)狀態(tài)"標(biāo)簽,需要使用開(kāi)關(guān)參數(shù)去激活,才可以使用
-XX: +UnlockExperimentalVMOptions
-XX: +UseJVMCICompiler
AOT編譯器
JDK9引入了AOT編譯器(靜態(tài)提前編譯器,Ahead Of Time Compiler)
Java 9引入了實(shí)驗(yàn)性AOT編譯工具jaotc,它借助了Graal 編譯器,將所輸入的Java 類文件轉(zhuǎn)換為機(jī)器碼,并存放至生成的動(dòng)態(tài)共享庫(kù)之中.
所謂AOT編譯,是與即時(shí)編譯相對(duì)立的一個(gè)概念.
我們知道,即時(shí)編譯指的是在程序的運(yùn)行過(guò)程中,將字節(jié)碼轉(zhuǎn)換為可在硬件上直接運(yùn)行的機(jī)器碼,并部署至托管環(huán)境中的過(guò)程.
而AOT編譯指的則是,在程序運(yùn)行之前,便將字節(jié)碼轉(zhuǎn)換為機(jī)器碼的過(guò)程.
最大好處:
Java虛擬機(jī)加載已經(jīng)預(yù)編譯成二進(jìn)制庫(kù),可以直接執(zhí)行.不必等待即時(shí)編譯器的預(yù)熱,減少Java應(yīng)用給人帶來(lái)“第一次運(yùn)行慢”的不良體驗(yàn)
缺點(diǎn):
破壞了java"一次編譯,到處運(yùn)行”(提前干掉了能夠跨平臺(tái)的class文件),必須為每個(gè)不同硬件、oS編譯對(duì)應(yīng)的發(fā)行包.
降低了Java鏈接過(guò)程的動(dòng)態(tài)性,加載的代碼在編譯期就必須全部已知.
還需要繼續(xù)優(yōu)化中,最初只支持Linux x64 java base
請(qǐng)關(guān)注我的訂閱號(hào)
參考
- 《深入理解JAVA虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》