JVM——逃逸分析
首先,為解釋這個問題,需要的基本知識如下(如果對以下概念不太熟悉, 可以先Google下):
1.JVM內存結構,傳送門
2.即時編譯(JIT),傳送門
逃逸分析
在編譯期間,JIT會對代碼做很多優化。其中有一部分優化的目的就是減少內存堆分配壓力,其中一種重要的技術叫做逃逸分析。
逃逸分析(Escape Analysis)是目前Java虛擬機中比較前沿的優化技術。這是一種可以有效減少Java 程序中 同步負載 和 內存堆分配壓力 的 跨函數全局數據流分析算法。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行為就是分析對象動態作用域:當一個對象在方法中被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他地方中,稱為方法逃逸。
例如:
public static StringBuffer newStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }StringBuffer sb是一個方法內部變量,上述代碼中直接將sb返回,這樣這個StringBuffer有可能被其他方法所改變,這樣它的作用域就不只是在方法內部,雖然它是一個局部變量,稱其逃逸到了方法外部。甚至還有可能被外部線程訪問到,譬如賦值給類變量或可以在其他線程中訪問的實例變量,稱為線程逃逸。
上述代碼如果想要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將不會逃逸出方法。
使用逃逸分析,編譯器可以對代碼做如下優化:
一、同步省略:如果一個對象被發現只能從一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
二、將堆分配轉化為棧分配:如果一個對象在子程序中被分配,如果指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。
三、分離對象或標量替換。有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。
本文主要來介紹逃逸分析的第二個用途:將堆分配轉化為棧分配
其實,以上三種優化中,棧上內存分配其實是依靠標量替換來實現的。由于不是本文重點,這里就不展開介紹了。如果大家感興趣,我后面專門出一篇文章,全面介紹下逃逸分析。
在Java代碼運行時,通過JVM參數可指定是否開啟逃逸分析,
-XX:+DoEscapeAnalysis : 表示開啟逃逸分析
-XX:-DoEscapeAnalysis : 表示關閉逃逸分析
從jdk 1.7開始已經默認開始逃逸分析,如需關閉,需要指定-XX:-DoEscapeAnalysis
對象的棧上內存分配
我們知道,在一般情況下,對象和數組元素的內存分配是在堆內存上進行的。但是隨著JIT編譯器的日漸成熟,很多優化使這種分配策略并不絕對。JIT編譯器就可以在編譯期間根據逃逸分析的結果,來決定是否可以將對象的內存分配從堆轉化為棧。
我們來看以下代碼:
public static void main(String[] args) {long a1 = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {alloc();}// 查看執行時間long a2 = System.currentTimeMillis();System.out.println("cost " + (a2 - a1) + " ms");// 為了方便查看堆內存中對象個數,線程sleeptry {Thread.sleep(100000);} catch (InterruptedException e1) {e1.printStackTrace();} }private static void alloc() {User user = new User(); }static class User {}其實代碼內容很簡單,就是使用for循環,在代碼中創建100萬個User對象。
我們在alloc方法中定義了User對象,但是并沒有在方法外部引用他。也就是說,這個對象并不會逃逸到alloc外部。經過JIT的逃逸分析之后,就可以對其內存分配進行優化。
我們指定以下JVM參數并運行:
-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError在程序打印出 cost XX ms 后,代碼運行結束之前,我們使用[jmap][1]命令,來查看下當前堆內存中有多少個User對象:
? ~ 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執行結果中我們可以看到,堆中共創建了100萬個StackAllocTest$User實例。
在關閉逃避分析的情況下(-XX:-DoEscapeAnalysis),雖然在alloc方法中創建的User對象并沒有逃逸到方法外部,但是還是被分配在堆內存中。也就說,如果沒有JIT編譯器優化,沒有逃逸分析技術,正常情況下就應該是這樣的。即所有對象都分配到堆內存中。
接下來,我們開啟逃逸分析,再來執行下以上代碼。
-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError在程序打印出 cost XX ms 后,代碼運行結束之前,我們使用jmap命令,來查看下當前堆內存中有多少個User對象:
? ~ 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從以上打印結果中可以發現,開啟了逃逸分析之后(-XX:+DoEscapeAnalysis),在堆內存中只有8萬多個StackAllocTest$User對象。也就是說在經過JIT優化之后,堆內存中分配的對象數量,從100萬降到了8萬。
除了以上通過jmap驗證對象個數的方法以外,讀者還可以嘗試將堆內存調小,然后執行以上代碼,根據GC的次數來分析,也能發現,開啟了逃逸分析之后,在運行期間,GC次數會明顯減少。正是因為很多堆上分配被優化成了棧上分配,所以GC次數有了明顯的減少。
總結
所以,如果以后再有人問你:是不是所有的對象和數組都會在堆內存分配空間?
那么你可以告訴他:不一定,隨著JIT編譯器的發展,在編譯期間,如果JIT經過逃逸分析,發現有些對象沒有逃逸出方法,那么有可能堆內存分配會被優化成棧內存分配。但是這也并不是絕對的。就像我們前面看到的一樣,在開啟逃逸分析之后,也并不是所有User對象都沒有在堆上分配。
轉自Hollis
總結
- 上一篇: 深入理解 JVM Class文件格式(十
- 下一篇: 减肥可以吃鸭胗吗