一捌治、基本原理與名詞
逃逸分析是目前較前沿的優(yōu)化技術(shù),它不會(huì)進(jìn)行代碼的直接優(yōu)化胞谈,而是為其他優(yōu)化技術(shù)提供分析的技術(shù)尘盼。
原理
通過(guò)其對(duì)象動(dòng)態(tài)作用域進(jìn)行分析,從而得到逃逸程度烦绳。
方法逃逸
當(dāng)一個(gè)對(duì)象在方法里面被定義后卿捎,它可能被外部方法所引用,作為調(diào)用參數(shù)傳遞到其他方法中径密。
線程逃逸
賦值到可以在其他線程中訪問(wèn)到的實(shí)例變量午阵。
逃逸程度
逃逸程度從低到高分為三個(gè)級(jí)別:
- 不逃逸:其他方法或線程都無(wú)法通過(guò)任何途徑訪問(wèn)到這個(gè)對(duì)象。
- 逃逸程度低:即方法逃逸,線程以?xún)?nèi)的逃逸底桂。
- 逃逸程度高:即線程逃逸植袍,可逃逸至線程外。
二籽懦、逃逸優(yōu)化
1. 棧上分配
當(dāng)確定一個(gè)對(duì)象不會(huì)逃逸出線程之外于个,直接讓對(duì)象在棧上進(jìn)行內(nèi)存分配 即可,對(duì)象占用的內(nèi)存會(huì)隨棧幀出棧而銷(xiāo)毀暮顺。
在實(shí)際應(yīng)用開(kāi)發(fā)中厅篓,不逃逸和逃逸程度低的對(duì)象所占比例是很大的,大量對(duì)象隨著方法結(jié)束會(huì)自動(dòng)銷(xiāo)毀捶码,垃圾收集的壓力會(huì)大大減小贷笛。但此方式 不支持線程逃逸。
2. 標(biāo)量替換
標(biāo)量
一個(gè)數(shù)據(jù)已無(wú)法再分解成更小的數(shù)據(jù)來(lái)表示宙项,那么就可以稱(chēng)它為標(biāo)量乏苦。
例如:Java 虛擬機(jī)的原始數(shù)據(jù)類(lèi)型(int
、long
等數(shù)值類(lèi)型及 reference
類(lèi)型等)尤筐,都無(wú)法進(jìn)一步進(jìn)行分解汇荐。
聚合量:
一個(gè)數(shù)據(jù)可以繼續(xù)分解,則為聚合量盆繁。
例如:Java 中的對(duì)象就是典型的聚合量掀淘。
標(biāo)量替換
如果把一個(gè)對(duì)象(聚合量)拆散,根據(jù)程序訪問(wèn)情況油昂,將用到的 成員變量恢復(fù)為原始類(lèi)型(標(biāo)量)來(lái)進(jìn)行訪問(wèn)革娄,此過(guò)程即為標(biāo)量替換。
當(dāng)確定一個(gè)對(duì)象不會(huì)被方法外部訪問(wèn)冕碟,并且這個(gè)對(duì)象可以被拆散拦惋,則程序執(zhí)行時(shí)可能不會(huì)創(chuàng)建這個(gè)對(duì)象,由此帶來(lái)兩點(diǎn)好處:
- 對(duì)象可直接分配和讀寫(xiě)在棧上安寺,而棧上的數(shù)據(jù)很大機(jī)會(huì)被分配至物理機(jī)器的高速寄存器中存儲(chǔ)厕妖。
- 為后續(xù)進(jìn)一步優(yōu)化創(chuàng)造條件。
但此方式要求更高挑庶,不允許對(duì)象逃逸出方法范圍內(nèi)言秸。
3. 同步消除
線程同步本身是一個(gè)相對(duì)耗時(shí)的過(guò)程迎捺,當(dāng)確定一個(gè)變量不會(huì)逃逸出線程時(shí)(其他線程無(wú)法訪問(wèn)),此變量讀寫(xiě)肯定不會(huì)有競(jìng)爭(zhēng)凳枝,對(duì)這個(gè)變量進(jìn)行的 同步措施可以安全消除。
4. 工作過(guò)程示例
初始代碼
class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public int test(int x) {
int xx = x + 2;
Point p = new Point(xx, 42);
}
內(nèi)聯(lián)化
將 Point
的構(gòu)造函數(shù)和 getX()
方法進(jìn)行內(nèi)聯(lián)優(yōu)化(參考內(nèi)聯(lián)相關(guān)博客)。
public int test(int x) {
int xx = x + 2;
Point p = point_memory_alloc(); // 在堆上分配 p 對(duì)象的表示(非真實(shí)代碼)
p.x = xx; // ```Point``` 的構(gòu)造函數(shù)內(nèi)聯(lián)后的表示(非真實(shí)代碼)
p.y = 42;
return p.x; // ```Point::getX()``` 被內(nèi)聯(lián)后的表示(非真實(shí)代碼)
}
逃逸分析(標(biāo)量替換)
整個(gè) test()
方法的范圍內(nèi) Point
對(duì)象實(shí)例不會(huì)發(fā)生任何程度的逃逸,故可進(jìn)行標(biāo)量替換優(yōu)化锭环。
把內(nèi)部 x 和 y 直接置換出來(lái)聪全,分解為 test()
方法內(nèi)的局部變量,從而不用直接實(shí)例化 Point 對(duì)象實(shí)例辅辩,達(dá)到優(yōu)化目的难礼。
public int test(int x) {
int xx = x + 2;
int px = xx;
int py = 42;
return px;
}
數(shù)據(jù)流分析
經(jīng)過(guò)數(shù)據(jù)流分析(參考數(shù)據(jù)流分析相關(guān)博客),發(fā)現(xiàn) py 的值不會(huì)對(duì)方法造成影響玫锋,故可直接消除優(yōu)化蛾茉。
public int test(int x) {
return x + 2;
}
三、實(shí)驗(yàn)(棧上分配驗(yàn)證)
測(cè)試代碼
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
alloc();
}
// 查看執(zhí)行時(shí)間
long end = System.currentTimeMillis();
System.out.println("cost " + (end - start) + " ms");
// 為了方便查看堆內(nèi)存中對(duì)象個(gè)數(shù)撩鹿,線程 sleep
try {
Thread.sleep(600000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User();
}
static class User {
}
分析
代碼內(nèi)容很簡(jiǎn)單谦炬,使用 for
循環(huán),創(chuàng)建 100 萬(wàn)個(gè) User
對(duì)象节沦。
其中键思,alloc
方法中定義了 User
對(duì)象,但是并沒(méi)有在方法外部引用甫贯。故這個(gè)對(duì)象并不會(huì)逃逸到 alloc
外部吼鳞。經(jīng)過(guò) JIT 的逃逸分析之后,就可以對(duì)其內(nèi)存分配進(jìn)行優(yōu)化叫搁。
參數(shù)設(shè)定
- 第一組
指定以下 JVM 參數(shù):
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
其中-XX:-DoEscapeAnalysis
表示 關(guān)閉 逃逸分析赔桌。 - 第二組
指定以下 JVM 參數(shù):
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
其中-XX:+DoEscapeAnalysis
表示 開(kāi)啟 逃逸分析。
運(yùn)行結(jié)果
分別使用兩組參數(shù)運(yùn)行代碼渴逻。
在程序打印出 cost XX ms 后疾党,代碼運(yùn)行結(jié)束之前,我們使用 jmap
命令惨奕,來(lái)查看下當(dāng)前堆內(nèi)存中有多少個(gè) User
對(duì)象仿贬。
> jmap -histo 2809 // 其中 2809 為當(dāng)前 JVM 進(jìn)程 ID
- 第一組
num #instances #bytes class name
----------------------------------------------
1: 524 87282184 [I
2: 1000000 16000000 StackAllocTest$User
3: 6806 2093136 [B
4: 8006 1320872 [C
5: 4188 100512 java.lang.String
6: 581 66304 java.lang.Class
- 第二組
num #instances #bytes class name
----------------------------------------------
1: 524 101944280 [I
2: 6806 2093136 [B
3: 83619 1337904 StackAllocTest$User
4: 8006 1320872 [C
5: 4188 100512 java.lang.String
6: 581 66304 java.lang.Class
分析
從上面的 jmap
執(zhí)行結(jié)果中我們可以看到。
- 第一組
堆中共創(chuàng)建了 100 萬(wàn)個(gè)StackAllocTest$User
實(shí)例墓贿。 - 第二組
堆中共創(chuàng)建了 8.3 萬(wàn)多個(gè)StackAllocTest$User
實(shí)例茧泪。
結(jié)論
在關(guān)閉逃避分析的情況下,雖然在 alloc
方法中創(chuàng)建的 User
對(duì)象并沒(méi)有逃逸到方法外部聋袋,但是還是被分配在堆內(nèi)存中队伟。
故沒(méi)有 JIT 編譯器優(yōu)化,沒(méi)有逃逸分析技術(shù)幽勒,所有對(duì)象都分配到堆內(nèi)存中嗜侮。
在打開(kāi)逃避分析的情況下,在堆內(nèi)存中只有 8 萬(wàn)多個(gè) StackAllocTest$User
對(duì)象。也就是說(shuō)在經(jīng)過(guò) JIT 優(yōu)化之后锈颗,堆內(nèi)存中分配的對(duì)象數(shù)量顷霹,從 100 萬(wàn)降到了 8.3 萬(wàn)。
除以上通過(guò) jmap
驗(yàn)證對(duì)象個(gè)數(shù)的方法以外击吱,還可以嘗試將堆內(nèi)存調(diào)小淋淀,然后執(zhí)行以上代碼覆醇,根據(jù)GC的次數(shù)來(lái)分析。
也能發(fā)現(xiàn)袍辞,開(kāi)啟了逃逸分析之后常摧,在運(yùn)行期間,GC 次數(shù)會(huì)明顯減少似芝。因?yàn)楹芏喽焉戏峙鋵?duì)象內(nèi)存被優(yōu)化至棧上分配板甘,隨著方法結(jié)束而自動(dòng)銷(xiāo)毀,導(dǎo)致 GC 次數(shù)減少寞奸。
四在跳、總結(jié)
發(fā)展與現(xiàn)狀
關(guān)于逃逸分析的論文在 1999 年就已經(jīng)發(fā)表了,但直到 JDK 1.6瓷翻,HotSpot 才初步支持逃逸分析實(shí)現(xiàn)齐帚,而且這項(xiàng)技術(shù)到如今也并不是十分成熟的彼哼,仍有很大的改進(jìn)余地。
根本原因
無(wú)法保證逃逸分析的性能消耗一定能高于他的消耗剪菱。雖然經(jīng)過(guò)逃逸分析可以做標(biāo)量替換、棧上分配和同步消除孝常。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對(duì)耗時(shí)的過(guò)程上渴。
舉一個(gè)極端的例子驰贷,經(jīng)過(guò)逃逸分析之后洛巢,發(fā)現(xiàn)沒(méi)有一個(gè)對(duì)象是不逃逸的次兆,那這個(gè)逃逸分析的過(guò)程就白白浪費(fèi)掉了。故目前虛擬機(jī)只能采用不那么準(zhǔn)確漓库,但時(shí)間壓力相對(duì)小的算法。
雖然這項(xiàng)技術(shù)并不十分成熟渺蒿,但是他也是即時(shí)編譯器優(yōu)化技術(shù)中一個(gè)十分重要的手段茂装。