您好,登錄后才能下訂單哦!
要了解逃逸分析背后的基本原理,我們先來看下這段有問題的C代碼——當(dāng)然這個是沒法用Java來寫的:
這段C代碼在棧上創(chuàng)建了一個int類型的變量,然后把它的指針作為函數(shù)的返回值返回了。這樣做是有問題的,因為當(dāng)gettheint()函數(shù)返回的時候,int所在的棧幀就已經(jīng)被銷毀了,后面你再去訪問這個地址的話,就不知道里面存儲的到底是什么了。
Java平臺設(shè)計的一個主要目標(biāo)就是要消除這種類型的bug。從設(shè)計上,JVM就不具備這種低級的“根據(jù)位置索引來讀內(nèi)存”的能力。這類操作對應(yīng)的Java字節(jié)碼是putfield和getfield。
來看下這段Java代碼:
這段代碼創(chuàng)建了一億對隨機(jī)大小的矩形,并去計算有多少對是大小一樣的。每次迭代都會創(chuàng)建一對新的矩形。你可能會認(rèn)為main方法里會創(chuàng)建2億個Rect對象:一億個r1,一億個r2。
不過,如果某個對象只是在方法內(nèi)部創(chuàng)建并使用的話——也就是說,它不會傳遞到另一個方法中或者作為返回值返回——那么運(yùn)行時程序就還能做得更聰明一些。你可以說這個對象是沒有逃逸出去的,因此運(yùn)行時(其實(shí)就是JIT編譯器)做的這個分析又叫做逃逸分析。
如果一個對象沒有逃逸出去,那也就是說JVM可以針對這個對象做一些類似“棧自動分配”的事情。在這個例子當(dāng)中,這個對象不會從堆上分配空間,因此它也不需要垃圾回收器來回收。一旦使用這個“棧分配(stack-allocated)”對象的方法返回了,這個對象所占用的內(nèi)存也就自動被釋放掉了。
事實(shí)上,HotSpot VM的C2編譯器做的事情要比棧分配要復(fù)雜得多。我們現(xiàn)在就來看一下。
在HotSpot VM的源碼中,可以看到逃逸分析系統(tǒng)是如何對對象的使用進(jìn)行分類的:
第一類說明這個對象可以用標(biāo)量來代替。這種分配消除技術(shù)叫標(biāo)量替換(scalar replacement)。這意味著這個對象會被拆解成它的構(gòu)成字段,這就相當(dāng)于分配對象的操作變成了在方法內(nèi)部創(chuàng)建多個局部變量。完成這個之后,另一項HotSpot VM的JIT技術(shù)會參與進(jìn)來,它會將這些字段(事實(shí)上已經(jīng)是局部變量了)存儲到CPU的寄存器中(如果有必要就存儲在棧上)。
Java平臺的主要挑戰(zhàn)是執(zhí)行模型非常復(fù)雜。在上述例子中,如果只看源代碼,你會認(rèn)為r1對象是不會逃逸出main方法外的,但r2會作為參數(shù)傳給r1的sameArea方法,因此它逃逸出了main方法外。
根據(jù)上面的分類,乍一看的話r1應(yīng)該歸類為NoEscape,而r2應(yīng)該歸為ArgEscape;不過這個結(jié)論是錯誤的,原因有幾點(diǎn)。
第一,回想一下,Java中的方法調(diào)用最終會通過編譯器替換為字節(jié)碼invoke。它會把調(diào)用目標(biāo)(也就是接收對象,注:即要調(diào)用的對象)和入?yún)⑻畛涞綏V?,然后查找到這個方法,再分發(fā)給它(也就是執(zhí)行這個方法)。
這意味著接收對象也被傳入了調(diào)用的方法中(它就是調(diào)用的方法里的this對象)。因此接收對象也逃逸出了當(dāng)前域;在這個例子中,這意味著如果逃逸分析分析完這段Java代碼,r1和r2都會歸類為ArgEscape。
如果就只是這樣的話,那么分配消除的使用場景就很有限了。所幸的是,HotSpot VM能做得更好。我們來仔細(xì)看一下它的字節(jié)碼,看看能發(fā)現(xiàn)什么。
sameArea()方法很?。ㄖ挥?7個字節(jié)的字節(jié)碼),在本例中也會被頻繁調(diào)用,因此它是方法內(nèi)聯(lián)(method inlined)的一個理想對象。
這個方法又調(diào)用了兩次area()方法(這個也是可以內(nèi)聯(lián)的):
通過JITWatch或者PrintCompilation可以看到,area()方法的調(diào)用的確被內(nèi)聯(lián)進(jìn)了調(diào)用方sameArea()方法里,而sameArea()又被內(nèi)聯(lián)到了main()方法的循環(huán)體中。JITWatch為內(nèi)聯(lián)方法提供了一個很方便的圖形化展示(如圖一所示)。
請記住Java HotSpot VM的JIT編譯器的優(yōu)化順序也是很重要的。方法內(nèi)聯(lián)是最早的優(yōu)化,也被稱為網(wǎng)關(guān)優(yōu)化(gateway optimization),因為它首先把相關(guān)聯(lián)的代碼都聚合在了一起,為其它優(yōu)化打開了大門。http://dalian.huodong.dqccc.com/exposition/detail-2237268.html
鄭州男人不孕不育醫(yī)院:http://www.xbzztj.com/
現(xiàn)在sameArea()方法和area()方法都被內(nèi)聯(lián)進(jìn)來了,方法域的問題不復(fù)存在,所有的變量都只在main方法的作用域內(nèi)了。也就是說逃逸分析不會再把r1和r2視作ArgEscape類型:方法內(nèi)聯(lián)之后,它們現(xiàn)在都被歸類為NoEscape。
這個結(jié)果看起來可能有悖常理,不過你需要記住的是JIT編譯器并不是通過原始代碼來進(jìn)行優(yōu)化的。如果不知道這點(diǎn),就搞不清楚哪些情況能夠進(jìn)行逃逸分析。
前面的例子中,這些對象的分配都不會在堆上進(jìn)行了,會把它們的字段拆解成獨(dú)立的值。寄存器分配器通常會把拆解出來的字段直接放到寄存器中,不過如果沒有足夠可用的寄存器,那剩下的字段會被存儲到棧上。這種情況被稱為棧溢出(stack spill,注:和stack overflow不同)。
在逃逸分析開啟和關(guān)閉的模式下分別運(yùn)行這個程序,再觀察下GC的活動,你就能看到密集循環(huán)中堆分配消除的巨大威力。
在現(xiàn)代JVM中逃逸分析是默認(rèn)開啟的,得通過JVM參數(shù)-XX:-DoEscapeAnalysis來關(guān)掉它。
下面是開啟了逃逸分析之后的GC日志(一些細(xì)節(jié)刪除了):
從日志中可以看到根本沒有發(fā)生GC事件——只是在進(jìn)程退出時往日志里記錄了下堆的摘要信息。如果再看下關(guān)閉逃逸分析后的運(yùn)行日志,情況就截然不同了:
這里可以很清楚地看到,由于Eden區(qū)空間滿了,導(dǎo)致了內(nèi)存分配失敗、需要進(jìn)行垃圾回收,因此觸發(fā)了GC事件。
逃逸分析是Java HotSpot VM引入的一項非常有用的升級。這項功能仍在開發(fā)階段時,實(shí)際測試中它帶來的性能提升就有3%到6%。
對于那些對平臺特性的實(shí)現(xiàn)過程和原理感興趣的開發(fā)人員來說,逃逸分析有個很有意思的特點(diǎn):這項特性依賴于其它優(yōu)化(自動內(nèi)聯(lián)),不然用處不大。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。