天天看點

别讓Java對象逃逸(Object Escape)

翻譯:吳嘉俊,叩丁狼進階講師 

關于逃逸分析

我在開源項目Speedment的開發過程中,我和項目的貢獻者都意識到我們的代碼不僅要良好并易懂,同時還要有較高的性能,否則他們很容易轉向使用其他的解決方案。

逃逸分析(Escape Analysis)允許我們在寫出性能較好的代碼的同時,能通過恰當的抽象,保證良好的代碼風格。

逃逸分析(簡寫為“EA”)允許java編譯器在多種情況下優化我們的代碼。請考慮一下代碼:

public class Point {  

    private final int x, y;  

    public Point(int x, int y) {  
        this.x = x;  
        this.y = y;  
    }  

    @Override  
    public String toString() {  
        final StringBuilder sb = new StringBuilder()  
                .append("(")  
                .append(x)  
                .append(", ")  
                .append(y)  
                .append(")");  
        return sb.toString();  
    }  

}
           

每一次我們調用Point::toString方法的時候,都會建立一個新的StringBuilder對象。這個建立出來的對象,對于外面的方法或者運作目前代碼的其他線程都是不可見的(因為其他線程隻能看到自己的StringBuilder版本)。

是以,當我們大量的調用了這個方法之後,會出現大量的StringBuilder對象麼?不會。因為逃逸分析的作用,編譯器會可以在棧上為StringBuilder配置設定空間。是以,當我們的方法傳回的時候,對象會自動的被删除,棧上的指針會自動的回退到這個方法調用之前的值。

逃逸分析在Java中已經存在了很久了。在最開始的時候,我們需要在指令行選項中手動開啟逃逸分析(通過-XX:+DoEscapeAnalysis開啟),現在它已經作為預設開啟的選項了。Java8在以前的Java基礎上,對于逃逸分析,又有了新的增強。

逃逸分析如何工作

基于逃逸分析,一個對象會可能會被用三種逃逸狀态标記:

  • 全局級别逃逸:一個對象可能從一個方法或者目前線程中逃逸。再明确一點,如果一個對象被作為一個方法的傳回值,那麼對象被标記為全局逃逸狀态。如果一個對象作為類靜态字段(static field)或者類字段(field),同樣會被标記為全局逃逸狀态。另外,如果我們複寫了一個方法的finalize()方法,那麼這個類的對象都會被标記為全局逃逸狀态并且一定會放在堆記憶體中,這也符合情理,因為這些對象需要對于JVM的finalizer必須是可見的(是以發生逃逸了)。當然,還有其他的一些情況也會讓對象标記為全局逃逸狀态。
  • 參數級别逃逸:如果一個對象被作為參數傳遞給一個方法,但是在這個方法之外無法通路或者對其他線程不可見,這個對象标記為參數級别逃逸。
  • 無逃逸狀态:一個對象不會産生逃逸

标記為全局級别逃逸或者參數級别逃逸的對象必須在堆中配置設定空間,但是參數級别逃逸是可能在記憶體中去掉對象同步鎖的,因為上面已經解釋,參數級别逃逸對象不會被其他線程通路。

無逃逸狀态的對象的記憶體配置設定會更加自由,可能會在棧上配置設定,也可能會在堆上配置設定。事實上,在某些情況下,甚至根本不會去建立一個對象,而直接使用該對象的标量值代替,比如僅僅在棧上建立一個int,去代替一個Integer對象。因為隻有一個線程可以通路該對象,是以對象上的同步鎖自然會被去掉。例如,我們使用無逃逸狀态的StringBuffer(較之StringBuilder,StringBuffer是線程安全的,所有方法都是synchronized),那麼這種情況下,所有方法的同步鎖都會被去掉,提高執行效率。

EA目前隻在C2 HotSpot編譯器下有用,是以請確定我們運作在-server模式下。

為什麼它很重要

理論上來說,無逃逸狀态對象可以直接在棧上配置設定記憶體,甚至直接在CPU寄存器中配置設定空間,以提供非常快速的執行。

當我們在堆上配置設定空間的時候,因為對象可能會被配置設定在不連續的彼此遠離的堆位址上,這種位址配置設定方式會非常快的耗盡我們L1 CPU緩存,性能會受到影響。而當使用EA通過棧來配置設定空間,在大部分情況下,使用的都是L1緩存中已經配置設定的空間。是以,EA在棧上配置設定空間,會在資料存儲位置配置設定上面更加優化。這對于性能是有益的。

當我們使用EA在棧上配置設定空間,垃圾回收器的工作量會極大減低,這可能是EA帶來的一個最大的性能提升點。因為每一次在垃圾回收執行的時候,會對堆進行一次完整的掃描,這對于我們的CPU性能,和CPU緩存的消耗是非常大的。更不用說,如果伺服器部分虛拟記憶體在獨立的儲存設備上,過于頻繁的GC帶來的絕對是災難性的影響。

EA帶來的最重要的提升不僅僅展現在性能上。EA允許我們使用本地抽象(local abstractions),比如Lambdas, Functions, Streams, Iterators等等。依賴于EA,我們能輕松的寫出既易讀,性能又高的代碼,讓代碼專注于我們在做的事情。

一個例子

public class Main {  

    public static void main(String[] args) throws IOException {  
        Point p = new Point(100, 200);  

        sum(p);  
        System.gc();  
        System.out.println("Press any key to continue");  
        System.in.read();  
        long sum = sum(p);  

        System.out.println(sum);  
        System.out.println("Press any key to continue2");  
        System.in.read();  

        sum = sum(p);  

        System.out.println(sum);  
        System.out.println("Press any key to exit");  
        System.in.read();  

    }  

    private static long sum(Point p) {  
        long sumLen = 0;  
        for (int i = 0; i < 1_000_000; i++) {  
            sumLen += p.toString().length();  
        }  
        return sumLen;  

    }  

}
           

上面的代碼中,建立了一個Point執行個體,并且通過調用sum方法,大量的調用Point對象的toString()方法。我們分了三個階段,首先,執行一次sum,然後立刻GC掉所有建立的對象,接下來兩次我們又調用兩次sum方法,但沒有從堆中删除任何東西,我們來驗證一下每一步執行之後堆的狀态。

我們使用如下的參數來執行應用,我們就可以看到在JVM中發生了什麼:

-server
-XX:BCEATraceLevel=3
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
-verbose:gc
-XX:MaxInlineSize=256
-XX:FreqInlineSize=1024
-XX:MaxBCEAEstimateSize=1024
-XX:MaxInlineLevel=22
-XX:CompileThreshold=10
-Xmx4g
-Xms4g
           

大量的運作參數來更清楚的讓我們看到到底發生了什麼。

當第一步調用完成,我們來看看堆的使用(在System.gc()執行之後)

pemi$ jps | grep Main
50903 Main
pemi$ jps | grep Main
50903 Main
pemi$ jmap -histo 50903 | head
 num     #instances         #bytes  class name

----------------------------------------------
   1:            95       42952184  [I
   2:          1079         101120  [C
   3:           485          55272  java.lang.Class
   4:           526          25936  [Ljava.lang.Object;
   5:            13          25664  [B
   6:          1057          25368  java.lang.String
   7:            74           5328  java.lang.reflect.Field
           

後面兩次調用完成之後:

pemi$ jmap -histo 50903 | head
 num     #instances         #bytes  class name
----------------------------------------------
   1:       2001080       88101152  [C
   2:           100       36777992  [I
   3:       1001058       24025392  java.lang.String
   4:         64513        1548312  java.lang.StringBuilder
   5:           485          55272  java.lang.Class
   6:           526          25936  [Ljava.lang.Object;
   7:            13          25664  [B


pemi$ jmap -histo 50903 | head
 num     #instances         #bytes  class name
----------------------------------------------
   1:       4001081      176101184  [C
   2:       2001059       48025416  java.lang.String
   3:           105       32152064  [I
   4:         64513        1548312  java.lang.StringBuilder
   5:           485          55272  java.lang.Class
   6:           526          25936  [Ljava.lang.Object;
   7:            13          25664  [B
           

可以看到,EA最終能删掉在堆上建立的StringBuilder執行個體。兩個操作對比,一個隻需要63K空間,另一個需要2M。确實是一個很大的進步。

原文位址:https://minborgsjavapot.blogspot.com/2015/12/do-not-let-your-java-objects-escape.html

别讓Java對象逃逸(Object Escape)