JVM——逃逸分析
首先,為解釋這個(gè)問(wèn)題,需要的基本知識(shí)如下(如果對(duì)以下概念不太熟悉, 可以先Google下):
1.JVM內(nèi)存結(jié)構(gòu),傳送門
2.即時(shí)編譯(JIT),傳送門
逃逸分析
在編譯期間,JIT會(huì)對(duì)代碼做很多優(yōu)化。其中有一部分優(yōu)化的目的就是減少內(nèi)存堆分配壓力,其中一種重要的技術(shù)叫做逃逸分析。
逃逸分析(Escape Analysis)是目前Java虛擬機(jī)中比較前沿的優(yōu)化技術(shù)。這是一種可以有效減少Java 程序中 同步負(fù)載 和 內(nèi)存堆分配壓力 的 跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。通過(guò)逃逸分析,Java Hotspot編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。
逃逸分析的基本行為就是分析對(duì)象動(dòng)態(tài)作用域:當(dāng)一個(gè)對(duì)象在方法中被定義后,它可能被外部方法所引用,例如作為調(diào)用參數(shù)傳遞到其他地方中,稱為方法逃逸。
例如:
public static StringBuffer newStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }StringBuffer sb是一個(gè)方法內(nèi)部變量,上述代碼中直接將sb返回,這樣這個(gè)StringBuffer有可能被其他方法所改變,這樣它的作用域就不只是在方法內(nèi)部,雖然它是一個(gè)局部變量,稱其逃逸到了方法外部。甚至還有可能被外部線程訪問(wèn)到,譬如賦值給類變量或可以在其他線程中訪問(wèn)的實(shí)例變量,稱為線程逃逸。
上述代碼如果想要StringBuffer sb不逃出方法,可以這樣寫:
public static String newStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); }不直接返回 StringBuffer,那么StringBuffer將不會(huì)逃逸出方法。
使用逃逸分析,編譯器可以對(duì)代碼做如下優(yōu)化:
一、同步省略:如果一個(gè)對(duì)象被發(fā)現(xiàn)只能從一個(gè)線程被訪問(wèn)到,那么對(duì)于這個(gè)對(duì)象的操作可以不考慮同步。
二、將堆分配轉(zhuǎn)化為棧分配:如果一個(gè)對(duì)象在子程序中被分配,如果指向該對(duì)象的指針永遠(yuǎn)不會(huì)逃逸,對(duì)象可能是棧分配的候選,而不是堆分配。
三、分離對(duì)象或標(biāo)量替換。有的對(duì)象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)存在也可以被訪問(wèn)到,那么對(duì)象的部分(或全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ)在CPU寄存器中。
本文主要來(lái)介紹逃逸分析的第二個(gè)用途:將堆分配轉(zhuǎn)化為棧分配
其實(shí),以上三種優(yōu)化中,棧上內(nèi)存分配其實(shí)是依靠標(biāo)量替換來(lái)實(shí)現(xiàn)的。由于不是本文重點(diǎn),這里就不展開(kāi)介紹了。如果大家感興趣,我后面專門出一篇文章,全面介紹下逃逸分析。
在Java代碼運(yùn)行時(shí),通過(guò)JVM參數(shù)可指定是否開(kāi)啟逃逸分析,
-XX:+DoEscapeAnalysis : 表示開(kāi)啟逃逸分析
-XX:-DoEscapeAnalysis : 表示關(guān)閉逃逸分析
從jdk 1.7開(kāi)始已經(jīng)默認(rèn)開(kāi)始逃逸分析,如需關(guān)閉,需要指定-XX:-DoEscapeAnalysis
對(duì)象的棧上內(nèi)存分配
我們知道,在一般情況下,對(duì)象和數(shù)組元素的內(nèi)存分配是在堆內(nèi)存上進(jìn)行的。但是隨著JIT編譯器的日漸成熟,很多優(yōu)化使這種分配策略并不絕對(duì)。JIT編譯器就可以在編譯期間根據(jù)逃逸分析的結(jié)果,來(lái)決定是否可以將對(duì)象的內(nèi)存分配從堆轉(zhuǎn)化為棧。
我們來(lái)看以下代碼:
public static void main(String[] args) {long a1 = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {alloc();}// 查看執(zhí)行時(shí)間long a2 = System.currentTimeMillis();System.out.println("cost " + (a2 - a1) + " ms");// 為了方便查看堆內(nèi)存中對(duì)象個(gè)數(shù),線程sleeptry {Thread.sleep(100000);} catch (InterruptedException e1) {e1.printStackTrace();} }private static void alloc() {User user = new User(); }static class User {}其實(shí)代碼內(nèi)容很簡(jiǎn)單,就是使用for循環(huán),在代碼中創(chuàng)建100萬(wàn)個(gè)User對(duì)象。
我們?cè)赼lloc方法中定義了User對(duì)象,但是并沒(méi)有在方法外部引用他。也就是說(shuō),這個(gè)對(duì)象并不會(huì)逃逸到alloc外部。經(jīng)過(guò)JIT的逃逸分析之后,就可以對(duì)其內(nèi)存分配進(jìn)行優(yōu)化。
我們指定以下JVM參數(shù)并運(yùn)行:
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError在程序打印出 cost XX ms 后,代碼運(yùn)行結(jié)束之前,我們使用[jmap][1]命令,來(lái)查看下當(dāng)前堆內(nèi)存中有多少個(gè)User對(duì)象:
? ~ jps 2809 StackAllocTest 2810 Jps ? ~ jmap -histo 2809num #instances #bytes class name ----------------------------------------------1: 524 87282184 [I2: 1000000 16000000 StackAllocTest$User3: 6806 2093136 [B4: 8006 1320872 [C5: 4188 100512 java.lang.String6: 581 66304 java.lang.Class從上面的jmap執(zhí)行結(jié)果中我們可以看到,堆中共創(chuàng)建了100萬(wàn)個(gè)StackAllocTest$User實(shí)例。
在關(guān)閉逃避分析的情況下(-XX:-DoEscapeAnalysis),雖然在alloc方法中創(chuàng)建的User對(duì)象并沒(méi)有逃逸到方法外部,但是還是被分配在堆內(nèi)存中。也就說(shuō),如果沒(méi)有JIT編譯器優(yōu)化,沒(méi)有逃逸分析技術(shù),正常情況下就應(yīng)該是這樣的。即所有對(duì)象都分配到堆內(nèi)存中。
接下來(lái),我們開(kāi)啟逃逸分析,再來(lái)執(zhí)行下以上代碼。
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError在程序打印出 cost XX ms 后,代碼運(yùn)行結(jié)束之前,我們使用jmap命令,來(lái)查看下當(dāng)前堆內(nèi)存中有多少個(gè)User對(duì)象:
? ~ jps 709 2858 Launcher 2859 StackAllocTest 2860 Jps ? ~ jmap -histo 2859num #instances #bytes class name ----------------------------------------------1: 524 101944280 [I2: 6806 2093136 [B3: 83619 1337904 StackAllocTest$User4: 8006 1320872 [C5: 4188 100512 java.lang.String6: 581 66304 java.lang.Class從以上打印結(jié)果中可以發(fā)現(xiàn),開(kāi)啟了逃逸分析之后(-XX:+DoEscapeAnalysis),在堆內(nèi)存中只有8萬(wàn)多個(gè)StackAllocTest$User對(duì)象。也就是說(shuō)在經(jīng)過(guò)JIT優(yōu)化之后,堆內(nèi)存中分配的對(duì)象數(shù)量,從100萬(wàn)降到了8萬(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)楹芏喽焉戏峙浔粌?yōu)化成了棧上分配,所以GC次數(shù)有了明顯的減少。
總結(jié)
所以,如果以后再有人問(wèn)你:是不是所有的對(duì)象和數(shù)組都會(huì)在堆內(nèi)存分配空間?
那么你可以告訴他:不一定,隨著JIT編譯器的發(fā)展,在編譯期間,如果JIT經(jīng)過(guò)逃逸分析,發(fā)現(xiàn)有些對(duì)象沒(méi)有逃逸出方法,那么有可能堆內(nèi)存分配會(huì)被優(yōu)化成棧內(nèi)存分配。但是這也并不是絕對(duì)的。就像我們前面看到的一樣,在開(kāi)啟逃逸分析之后,也并不是所有User對(duì)象都沒(méi)有在堆上分配。
轉(zhuǎn)自Hollis
總結(jié)
- 上一篇: 深入理解 JVM Class文件格式(十
- 下一篇: 单例模式的创建方式