JVM棧上分配對(duì)象內(nèi)存與逃逸分析原理分析(Escape Analysis)
1 逃逸分析
JVM中較前沿的優(yōu)化技術(shù),它與類(lèi)型繼承關(guān)系分析一樣,并非直接優(yōu)化代碼,而是為其他優(yōu)化措施提供依據(jù)的分析技術(shù)。
1.1 基本原理
分析對(duì)象動(dòng)態(tài)作用域,當(dāng)一個(gè)對(duì)象在方法里面被定義后,它可能
被外部方法所引用
例如作為調(diào)用參數(shù)傳遞給其他方法,稱(chēng)為方法逃逸
被外部線程訪問(wèn)
譬如賦值給可以在其他線程中訪問(wèn)的實(shí)例變量,稱(chēng)為線程逃逸
從不逃逸 =》方法逃逸 =》線程逃逸,稱(chēng)為對(duì)象由低到高的不同逃逸程度。
如果能證明一個(gè)對(duì)象不會(huì)逃逸到方法或線程外(即別的方法或線程無(wú)法通過(guò)任何途徑訪問(wèn)到該對(duì)象),或逃逸程度較低(只逃逸出方法而不逃逸出線程),則可能為這個(gè)對(duì)象實(shí)例采取不同程度的優(yōu)化,如:
2 棧上分配(Stack Allocations)
由于復(fù)雜度等原因,HotSpot中目前暫時(shí)還沒(méi)有做這項(xiàng)優(yōu)化,但一些其他的虛擬機(jī)(如Excelsior JET)使用了該優(yōu)化。
JVM中,Java堆上分配創(chuàng)建對(duì)象的內(nèi)存空間是常識(shí),Java堆中的對(duì)象對(duì)各線程共享可見(jiàn),只要持有該對(duì)象的引用,就可訪問(wèn)到堆中存儲(chǔ)的對(duì)象數(shù)據(jù)。
虛擬機(jī)的GC子系統(tǒng)會(huì)回收堆中不再使用的對(duì)象,但回收動(dòng)作無(wú)論是標(biāo)記篩選出可回收對(duì)象,還是回收和整理內(nèi)存,都需耗費(fèi)大量資源。
如果確定一個(gè)對(duì)象不會(huì)逃逸出線程,那讓該對(duì)象在棧上分配內(nèi)存是個(gè)不錯(cuò)主意,對(duì)象所占用內(nèi)存空間就可隨棧幀出棧而銷(xiāo)毀。
在一般應(yīng)用中,完全不會(huì)逃逸的局部對(duì)象和不會(huì)逃逸出線程的對(duì)象所占的比例很大,如果能使用棧上分配,那大量對(duì)象就會(huì)隨方法結(jié)束而自動(dòng)銷(xiāo)毀,GC子系統(tǒng)壓力會(huì)下降很多。棧上分配可支持方法逃逸,但不能支持線程逃逸。
3 標(biāo)量替換(Scalar Replacement)
若一個(gè)數(shù)據(jù)已經(jīng)無(wú)法再分解成更小數(shù)據(jù)來(lái)表示,JVM中基礎(chǔ)數(shù)據(jù)類(lèi)型都不能再進(jìn)一步分解,這些數(shù)據(jù)可被稱(chēng)為標(biāo)量。
相對(duì)的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它就被稱(chēng)為聚合量(Aggregate),Java 中的對(duì)象就是典型的聚合量。如果把一個(gè)Java對(duì)象拆散,根據(jù)程序訪問(wèn)的情況,將其用到的成員變量恢復(fù)為原始類(lèi)型來(lái)訪問(wèn),該過(guò)程就稱(chēng)為標(biāo)量替換。
假如逃逸分析能夠證明一個(gè)對(duì)象不會(huì)被方法外部訪問(wèn),并且該對(duì)象可被分解,那么程序真正執(zhí)行時(shí)將可能不去創(chuàng)建該對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這方法使用的成員變量代替。
將對(duì)象拆分后,除可讓對(duì)象的成員變量在棧上 (棧上存儲(chǔ)的數(shù)據(jù),很大機(jī)會(huì)被虛擬機(jī)分配至物理機(jī)器的高速寄存器中存儲(chǔ))分配和讀寫(xiě)外,還可為后續(xù)進(jìn)步優(yōu)化創(chuàng)建條件。
標(biāo)量替換可視作棧上分配一種特例,實(shí)現(xiàn)更簡(jiǎn)單(不用考慮對(duì)象完整結(jié)構(gòu)的分配),但對(duì)逃逸程度的要求更高,它不允許對(duì)象逃逸出方法范圍內(nèi)。
4 同步消除(Synchronization Elimination)
線程同步本身是一個(gè)相對(duì)耗時(shí)過(guò)程,如果逃逸分析能確定一個(gè)變量不會(huì)逃逸出線程,無(wú)法被其他線程訪問(wèn),那么該變量讀寫(xiě)肯定不會(huì)有競(jìng)爭(zhēng), 對(duì)該變量實(shí)施的同步措施也可安全消除。
關(guān)于逃逸分析的研究論文早在1999年就已經(jīng)發(fā)表,但到JDK 6,HotSpot才開(kāi)始支持初步逃逸分析,到現(xiàn)在這優(yōu)化技術(shù)尚未足夠成熟。
不成熟的原因主要是逃逸分析的計(jì)算成本非常高,甚至不能保證逃逸分析帶來(lái)的性能收益會(huì)高于它的消耗。要百分之百準(zhǔn)確地判斷一個(gè)對(duì)象是否會(huì)逃逸,需要進(jìn)行一系列復(fù)雜的數(shù)據(jù)流敏感的過(guò)程間分析,才能確定程序各個(gè)分支執(zhí)行時(shí)對(duì)此對(duì)象的影響。
前面介紹即時(shí)編譯、提前編譯優(yōu)劣勢(shì)時(shí)提到了過(guò)程間分析這種大壓力的分析算法正是即時(shí)編譯的弱項(xiàng)??梢栽囅胍幌拢绻右莘治鐾戤吅蟀l(fā)現(xiàn)幾乎找不到幾個(gè)不逃逸的對(duì)象, 那這些運(yùn)行期耗用的時(shí)間就白白浪費(fèi)了,所以目前虛擬機(jī)只能采用不那么準(zhǔn)確,但時(shí)間壓力相對(duì)較小的算法來(lái)完成分析。
C和C++原生支持棧上分配(不使用new即可),而C#也支持值類(lèi)型,可以自然做到標(biāo)量替換(但并不會(huì)對(duì)引用類(lèi)型做這種優(yōu)化)。
在靈活運(yùn)用棧內(nèi)存方面,確實(shí)是Java的弱項(xiàng)。
在現(xiàn)在仍處于實(shí)驗(yàn)階段的Valhalla項(xiàng)目里,設(shè)計(jì)了新的inline關(guān)鍵字用于定義Java的內(nèi)聯(lián)類(lèi)型, 目的是實(shí)現(xiàn)與C#中值類(lèi)型相對(duì)標(biāo)的功能。有了這個(gè)標(biāo)識(shí)與約束,以后逃逸分析做起來(lái)就會(huì)簡(jiǎn)單很多。
下面通過(guò)一系列Java偽代碼的變化過(guò)程來(lái)模擬逃逸分析是如何工作的,向讀者展示逃逸分析能夠?qū)崿F(xiàn)的效果。
初始代碼如下所示:
// 完全未優(yōu)化代碼 public int test(int x) { int xx = x + 2; Point p = new Point(xx, 42); return p.getX(); }
1
2
3
4
5
6
此處省略了Point類(lèi)的代碼。第一步,將Point的構(gòu)造函數(shù)和getX()方法進(jìn)行內(nèi)聯(lián)優(yōu)化:
// 步驟1:構(gòu)造函數(shù)內(nèi)聯(lián)后的樣子 public int test(int x) { int xx = x + 2; Point p = point_memory_alloc(); // 在堆中分配P對(duì)象的示意方法 p.x = xx; // Point構(gòu)造函數(shù)被內(nèi)聯(lián)后的樣子 p.y = 42 return p.x; // Point::getX()被內(nèi)聯(lián)后的樣子 }
1
2
3
4
5
6
7
8
第二步,經(jīng)過(guò)逃逸分析,發(fā)現(xiàn)在整個(gè)test()方法的范圍內(nèi)Point對(duì)象實(shí)例不會(huì)發(fā)生任何程度的逃逸, 這樣可以對(duì)它進(jìn)行標(biāo)量替換優(yōu)化,把其內(nèi)部的x和y直接置換出來(lái),分解為test()方法內(nèi)的局部變量,從 而避免Point對(duì)象實(shí)例被實(shí)際創(chuàng)建,優(yōu)化后的結(jié)果如下所示:
// 步驟2:標(biāo)量替換后的樣子 public int test(int x) { int xx = x + 2; int px = xx; int py = 42 return px; }
1
2
3
4
5
6
7
第三步,通過(guò)數(shù)據(jù)流分析,發(fā)現(xiàn)py的值其實(shí)對(duì)方法不會(huì)造成任何影響,那就可以放心地去做無(wú)效 代碼消除得到最終優(yōu)化結(jié)果,如下所示:
// 步驟3:做無(wú)效代碼消除后的樣子 public int test(int x) { return x + 2; }
1
2
3
4
從測(cè)試結(jié)果來(lái)看,實(shí)施逃逸分析后的程序在MicroBenchmarks中往往能得到不錯(cuò)的成績(jī),但是在實(shí)際的應(yīng)用程序中,尤其是大型程序中反而發(fā)現(xiàn)實(shí)施逃逸分析可能出現(xiàn)效果不穩(wěn)定的情況,或分析過(guò)程耗時(shí)但卻無(wú)法有效判別出非逃逸對(duì)象而導(dǎo)致性能(即時(shí)編譯的收益)下降,所以曾經(jīng)在很長(zhǎng)的一段時(shí) 間里,即使是服務(wù)端編譯器,也默認(rèn)不開(kāi)啟逃逸分析(從JDK 6 Update 23開(kāi)始,服務(wù)端編譯器中開(kāi)始才默認(rèn)開(kāi)啟逃逸分析。
),甚至在某些版本(如JDK 6 Update 18)中還曾完全禁止這項(xiàng)優(yōu)化,一直到JDK 7時(shí)這項(xiàng)優(yōu)化才成為服務(wù)端編譯器默認(rèn)開(kāi)啟的選項(xiàng)。如果有需要,或者確認(rèn)對(duì)程序運(yùn)行有益,用戶(hù)也可以使用參數(shù)-XX:+DoEscapeAnalysis來(lái)手動(dòng)開(kāi)啟逃逸分析, 開(kāi)啟之后可以通過(guò)參數(shù)-XX:+PrintEscapeAnalysis來(lái)查看分析結(jié)果。有了逃逸分析支持之后,用戶(hù)可使用參數(shù)-XX:+EliminateAllocations來(lái)開(kāi)啟標(biāo)量替換,使用+XX:+EliminateLocks來(lái)開(kāi)啟同步消 除,使用參數(shù)-XX:+PrintEliminateAllocations查看標(biāo)量的替換情況。
盡管目前逃逸分析技術(shù)仍在發(fā)展之中,未完全成熟,但它是即時(shí)編譯器優(yōu)化技術(shù)的一個(gè)重要前進(jìn) 方向,在日后的Java虛擬機(jī)中,逃逸分析技術(shù)肯定會(huì)支撐起一系列更實(shí)用、有效的優(yōu)化技術(shù)。
參考
《深入理解 Java 虛擬機(jī)》
Java JVM
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶(hù)投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。