Java對(duì)象竟然會(huì)在棧上分配內(nèi)存?
1 逃逸分析
JVM中高深的優(yōu)化技術(shù),如同類繼承關(guān)系分析,該技術(shù)并非直接去優(yōu)化代碼,而是一種為其他優(yōu)化措施提供依據(jù)的分析技術(shù)。
分析對(duì)象的動(dòng)態(tài)作用域,當(dāng)某對(duì)象在方法里被定義后,它可能
- 方法逃逸
被外部方法引用,例如作為參數(shù)傳遞給其他方法
- 線程逃逸
被外部線程訪問,例如賦值給可以在其他線程中訪問的實(shí)例變量
所以 Java 對(duì)象由低到高的逃逸程度即為:
- 不逃逸 =》
- 方法逃逸 =》
- 線程逃逸
若能確定一個(gè)對(duì)象
- 不會(huì)逃逸到方法或線程外(即其它方法、線程無法訪問到該對(duì)象)
- 或逃逸程度較低(只逃逸出方法而不逃逸出線程)
則可為該對(duì)象實(shí)例采取不同程度的優(yōu)化方案。
2 優(yōu)化方案
2.1 棧上分配(Stack Allocations)
由于復(fù)雜度等原因,HotSpot中目前暫時(shí)還沒有做這項(xiàng)優(yōu)化,但一些其他的虛擬機(jī)(如Excelsior JET)使用了該優(yōu)化。
JVM的GC模塊會(huì)回收堆中不再使用的對(duì)象,但如下回收動(dòng)作
- 標(biāo)記篩選出可回收對(duì)象
- 回收和整理內(nèi)存
都需耗費(fèi)大量資源。
若確定一個(gè)對(duì)象不會(huì)逃逸出線程,那讓該對(duì)象在棧上分配內(nèi)存就是個(gè)不錯(cuò)主意,對(duì)象所占用內(nèi)存空間就可隨棧幀出棧而銷毀。
在一般應(yīng)用中,完全不會(huì)逃逸的局部對(duì)象和不會(huì)逃逸出線程的對(duì)象所占比例很大,若能使用棧上分配,則大量對(duì)象就會(huì)隨方法結(jié)束而自動(dòng)銷毀,GC系統(tǒng)壓力會(huì)下降很多。
棧上分配可支持方法逃逸,但不能支持線程逃逸。
2.2 標(biāo)量替換(Scalar Replacement)
2.2.1 標(biāo)量
若一個(gè)數(shù)據(jù)已經(jīng)無法再分解成更小數(shù)據(jù),JVM中的原始數(shù)據(jù)類型(如 int、long 等數(shù)值類型及 reference 類型)都不能再進(jìn)一步分解,這些數(shù)據(jù)即為標(biāo)量。
2.2.2 聚合量
若一個(gè)數(shù)據(jù)可繼續(xù)分解,則稱為聚合量(Aggregate),比如 Java 對(duì)象就是聚合量。
2.2.3 標(biāo)量替換
把一個(gè)Java對(duì)象拆散,根據(jù)程序訪問情況,將其用到的成員變量恢復(fù)為原始類型來訪問。
假如逃逸分析能證明一個(gè)對(duì)象不會(huì)被方法外部訪問,并且該對(duì)象可被分解,那么程序真正執(zhí)行時(shí)將可能不去創(chuàng)建該對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這方法使用的成員變量。
將對(duì)象拆分后:
- 可讓對(duì)象的成員變量在棧上 (棧上存儲(chǔ)的數(shù)據(jù),很大概率會(huì)被JVM分配至物理機(jī)器的高速寄存器中存儲(chǔ))分配和讀寫
- 為后續(xù)進(jìn)步優(yōu)化創(chuàng)建條件
2.2.4 適用場(chǎng)景
標(biāo)量替換可視為棧上分配一種特例,實(shí)現(xiàn)更簡(jiǎn)單(不用考慮對(duì)象完整結(jié)構(gòu)的分配),但對(duì)逃逸程度的要求更高,它不允許對(duì)象逃逸出方法范圍內(nèi)。
2.3 同步消除(Synchronization Elimination)
線程同步是個(gè)相對(duì)耗時(shí)的過程,若逃逸分析能確定一個(gè)變量不會(huì)逃逸出線程,即不會(huì)被其他線程訪問,則該變量的讀寫肯定不會(huì)有線程競(jìng)爭(zhēng), 也可安全消除對(duì)該變量實(shí)施的同步措施。
逃逸分析的論文在1999年就已發(fā)表,但到JDK 6,HotSpot才開始初步支持逃逸分析,至今該也尚未成熟,主要因?yàn)樘右莘治龅挠?jì)算成本高到無法保證帶來的性能收益會(huì)高于它的消耗。要百分百準(zhǔn)確判斷一個(gè)對(duì)象是否會(huì)逃逸,需進(jìn)行一系列復(fù)雜數(shù)據(jù)流敏感的過程間分析,才能確定程序各個(gè)分支執(zhí)行時(shí)對(duì)此對(duì)象的影響。過程間分析這種大壓力的分析算法正是即時(shí)編譯的弱項(xiàng)。試想,若逃逸分析完畢后發(fā)現(xiàn)幾乎找不到幾個(gè)不逃逸的對(duì)象, 那這些運(yùn)行期耗用的時(shí)間就白費(fèi)了,所以目前JVM只能采用不那么準(zhǔn)確,但時(shí)間壓力相對(duì)較小的算法來完成分析。
C和C++原生支持棧上分配(不使用new即可),靈活運(yùn)用棧內(nèi)存方面,Java的確是弱勢(shì)群體。
在現(xiàn)在仍處于實(shí)驗(yàn)階段的Valhalla項(xiàng)目,設(shè)計(jì)了新的inline關(guān)鍵字用于定義Java的內(nèi)聯(lián)類型, 對(duì)標(biāo)C#的值類型。有了該標(biāo)識(shí)與約束,以后逃逸分析做起來就會(huì)簡(jiǎn)單很多。
3 代碼實(shí)戰(zhàn)驗(yàn)證
3.1 全無優(yōu)化的代碼
- public int test(int x) {
- int xx = x + 2;
- Point p = new Point(xx, 42);
- return p.getX();
- }
3.2 優(yōu)化step1:內(nèi)聯(lián)構(gòu)造器和getX()方法
- public int test(int x) {
- int xx = x + 2;
- // 在堆中分配P對(duì)象
- Point p = point_memory_alloc();
- // Point構(gòu)造器被內(nèi)聯(lián)后
- p.x = xx;
- p.y = 42;
- // Point::getX()被內(nèi)聯(lián)后
- return p.x;
- }
優(yōu)化step2:標(biāo)量替換
逃逸分析后,發(fā)現(xiàn)在整個(gè)test()方法的范圍內(nèi)Point對(duì)象實(shí)例不會(huì)發(fā)生任何程度逃逸, 便可對(duì)它進(jìn)行標(biāo)量替換:把其內(nèi)部的x和y直接置換出來,分解為test()方法內(nèi)的局部變量,從而避免了Point對(duì)象實(shí)例的創(chuàng)建
- public int test(int x) {
- int xx = x + 2;
- int px = xx;
- int py = 42
- return px;
- }
step3:無效代碼消除
數(shù)據(jù)流分析,發(fā)現(xiàn)py的值其實(shí)對(duì)方法不會(huì)造成任何影響,那就可以放心地去做無效代碼消除得到最終優(yōu)化結(jié)果,如下所示:
- public int test(int x) {
- return x + 2;
- }
觀察測(cè)試結(jié)果,實(shí)施逃逸分析后的程序在MicroBenchmarks中往往能得到不錯(cuò)的成績(jī),但在實(shí)際應(yīng)用程序中,尤其是大型程序中反而發(fā)現(xiàn)實(shí)施逃逸分析可能出現(xiàn)效果不穩(wěn)定,或分析過程耗時(shí)但卻無法有效判別出非逃逸對(duì)象而導(dǎo)致性能(即時(shí)編譯的收益)下降,所以曾經(jīng)在很長(zhǎng)的一段時(shí)間,即使是服務(wù)端編譯器,也默認(rèn)不開啟逃逸分析(從JDK 6 Update 23開始,服務(wù)端編譯器中開始才默認(rèn)開啟逃逸分析。),甚至在某些版本(如JDK 6 Update 18)中還曾完全禁止這項(xiàng)優(yōu)化,一直到JDK 7時(shí)這項(xiàng)優(yōu)化才成為服務(wù)端編譯器默認(rèn)開啟的選項(xiàng)。
若有需要或確認(rèn)對(duì)程序有益,可使用參數(shù):
- -XX:+DoEscapeAnalysis 手動(dòng)開啟逃逸分析
開啟后可通過參數(shù):
- -XX:+PrintEscapeAnalysis 查看分析結(jié)果
有逃逸分析支持后,用戶可使用如下參數(shù):
- -XX:+EliminateAllocations 開啟標(biāo)量替換
- +XX:+EliminateLocks 開啟同步消除
- -XX:+PrintEliminateAllocations 查看標(biāo)量的替換情況
讓我們一起期待該JIT優(yōu)化技術(shù)之逃逸分析的發(fā)展。
參考
《深入理解 Java 虛擬機(jī)》
本文轉(zhuǎn)載自微信公眾號(hào)「JavaEdge」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系JavaEdge公眾號(hào)。