1. 程式人生 > 實用技巧 >逃逸分析-棧上分配

逃逸分析-棧上分配

使用逃逸分析-進行程式碼優化

  使用逃逸分析,編譯器可以對程式碼做如下優化:
  一、棧上分配。將堆分配轉化為棧分配。如果一個物件在子程式中被分配,要使指向該物件的指標永遠不會逃逸,物件可能是棧分配的候選,而不是堆分配。

    JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個物件並沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成後,繼續在呼叫棧內執行,r最後執行緒結束,棧空間被回收,區域性變數物件也被回收。這樣就無須進行垃圾回收了。

    常見的棧上分配的場景:在逃逸分析中,已經說明了。分別是給成員變數賦值、方法返回值、例項引用傳遞。

    

public class EscapeAnalysis {

    public EscapeAnalysis obj;

    /*
    方法返回EscapeAnalysis物件,發生逃逸
     */
    public EscapeAnalysis getInstance(){
        return obj == null? new EscapeAnalysis() : obj;
    }
    /*
    為成員屬性賦值,發生逃逸
     */
    public void setObj(){
        this.obj = new EscapeAnalysis();
    }
    //思考:如果當前的obj引用宣告為static的?仍然會發生逃逸。

    /*
    物件的作用域僅在當前方法中有效,沒有發生逃逸
     */
    public void useEscapeAnalysis(){
        EscapeAnalysis e = new EscapeAnalysis();
    }
    /*
    引用成員變數的值,發生逃逸
     */
    public void useEscapeAnalysis1(){
        EscapeAnalysis e = getInstance();
        //getInstance().xxx()同樣會發生逃逸
    }
}

  

  二、同步省略(鎖消除)。如果一個物件被發現只能從一個執行緒被訪問到,那麼對於這個物件的操作可以不考慮同步。

    執行緒同步的代價是相當高的,同步的後果是降低併發性和效能。在動態編譯同步塊的時候,JIT編譯器可以藉助逃逸分析來判斷同步塊所使用的鎖物件是否只能夠被一個執行緒訪問而沒有被髮布到其他執行緒。如果沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分程式碼的同步。這樣就能大大提高併發性和效能。這個取消同步的過程就叫同步省略,也叫鎖消除。

public class SynchronizedTest {
    public void f() {
        Object hollis 
= new Object(); synchronized(hollis) { System.out.println(hollis); } } }

  上述程式碼在執行的時候如果開啟了逃逸分析,JIT編譯器在編譯這個同步塊的時候就會取消對這部分程式碼的同步

  三、分離物件或標量替換。有的物件可能不需要作為一個連續的記憶體結構存在也可以被訪問到,那麼物件的部分(或全部)可以不儲存在記憶體,而是儲存在cPu暫存器中。

    標量(Scalar)是指一個無法再分解成更小的資料的資料。Java中的原始資料型別就是標量。
    相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中的物件就是聚合量,因為他可以分解成其他聚合量和標量。

    在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那麼經過JITr優化,就會把這個物件拆解成若干個其中包含的若干個成員變數來代替。這個過程就是標量替換。

    標量替換引數設定:引數-XX:+EliminateAllocations:開啟了標量替換(預設開啟),允許將物件打散分配在棧上。

public class ScalarReplace {
    public static class User {
        public int id;
        public String name;
    }

    public static void alloc() {
        User u = new User();//未發生逃逸 標量替換等價於User u = new User()-> int id, String name;
        u.id = 5;
        u.name = "www.atguigu.com";
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為: " + (end - start) + " ms");
    }
}

下面是逃逸分析-棧上分配的除錯結果:

  jvm開啟逃逸分析的引數:-XX:-DoEscapeAnalysis不開啟,-XX:+DoEscapeAnalysis 開啟(預設開啟)

  除錯類: 

/**
 * 棧上分配測試
 * -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
 * @author shkstart  [email protected]
 * @create 2020  10:31
 */
public class StackAllocation {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        // 檢視執行時間
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為: " + (end - start) + " ms");
        // 為了方便檢視堆記憶體中物件個數,執行緒sleep
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    private static void alloc() {
        User user = new User();//未發生逃逸
    }

    static class User {

    }
}

不開啟的情況:

開啟的情況:

當記憶體下調到256m時,不開啟逃逸分析,將會發生GC,而開啟了逃逸分析之後沒有發生GC並且比不開啟時執行速度要快;