首頁  >  文章  >  Java  >  3分鐘搞清楚 JVM逃逸分析

3分鐘搞清楚 JVM逃逸分析

Java后端技术全栈
Java后端技术全栈轉載
2023-08-15 16:39:171260瀏覽

作為一個合格java開發者都知道,基本上所有物件都是在堆上創建。但是,這裡還是沒有把話說絕對哈,指的是基本上所有

昨天一位朋友在面試中,就說了所有物件都在堆中創建,然後背面試官一陣的嘲笑。

開始我們的正文,我們今天來聊聊關於逃逸分析。

逃逸分析(Escape Analysis)是目前Java虛擬機器中較前沿的最佳化技術。這是一種可以有效減少Java 程式中同步負載和記憶體堆分配壓力的跨函數全域資料流分析演算法。透過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的引用的使用範圍從而決定是否要將這個物件分配到堆上。

逃逸分析的基本原理是:分析物件動態作用域,當一個物件在方法裡面被定義後,它可能被外部方法所引用,例如作為呼叫參數傳遞到其他方法中,這種稱為方法逃逸;甚至還有可能被外部執行緒存取到,譬如賦值給可以在其他執行緒中存取的實例變量,這種稱為執行緒逃逸;從不逃逸、方法逃逸到執行緒逃逸,稱為物件由低到高的不同逃逸程度。

開啟逃逸分析,編譯器可以對程式碼進行如下最佳化

  1. #同步消除:如果一個物件被逃逸分析發現只能被一個執行緒所訪問,那對於這個物件的操作可以不同步。
  2. 堆疊上分配:如果確定物件不會逃逸出執行緒之外,那麼讓這個物件在堆疊上分配記憶體將會是一個很不錯的主意,而物件所佔用的記憶體空間就可以隨堆疊幀出棧而銷毀。
  3. 標量替換:如果一個物件被逃逸分析發現不會被外部方法訪問,並且這個物件可以拆散,那麼程式真正執行的時候將可能不去建立這個對象,而改為直接創建它的若干個比這個方法使用的成員變數來代替。 將物件拆分後,可以讓物件的成員變數在堆疊上分配和讀寫

JVM中透過以下參數可以指定是否開啟逃逸分析

##-XX: DoEscapeAnalysis :表示開啟逃逸分析(JDK 1.7之後預設開啟)。

-XX:-DoEscapeAnalysis :表示關閉逃脫分析。

同步消除

線程同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變數不會逃逸出線程,無法被其他線程訪問,那麼這個變數的讀寫肯定就不會有競爭,對這個變數實施的同步措施也就可以安全地消除掉。

如以下程式碼:

public void method() {
    Object o = new Object();
    synchronized (o) {
        System.out.println(o);
    }
}

物件o加鎖,但是物件o的生命週期與方法method()一樣,所以不會被其他執行緒存取到,不會發生執行緒安全性問題,那麼在JIT編譯階段會被最佳化為如下所示:

public void method() {
    Object o = new Object();
    System.out.println(o);
}

這也稱為鎖定消除

堆疊上分配

在Java虛擬機器中,Java堆疊上分配建立物件的記憶體空間幾乎是Java程式設計師都知道的常識,Java堆中的物件對於各個執行緒都是共享且可見的,只要持有這個物件的引用,就可以存取到堆中儲存的物件資料。虛擬機器的垃圾收集子系統會回收堆中不再使用的對象,但回收動作無論是標記篩選出可回收對象,還是回收和整理內存,都需要耗費大量資源。但是,存在一種特殊情況,如果逃逸分析確認物件不會逃逸出執行緒之外,那麼就可能被最佳化成堆疊上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。

如下列程式碼:

public static void main(String[] args) throws InterruptedException {

    for (int i = 0; i < 1000000; i++) {
        alloc();
    }

    Thread.sleep(100000);
}

private static void alloc() {
    User user = new User();
}

程式碼很簡單,就是循環建立100萬次,使用alloc()方法建立100萬個User物件。這裡的alloc()方法中定義了User物件並沒有被其他方法引用,所以符合堆疊上分配的要求。

JVM參數如下:

-Xmx2G -Xms2G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

啟動程序,透過jmap工具檢視實例數:

jmap -histo pid

num     #instances         #bytes  class name
----------------------------------------------
1:          3771        2198552  [B
2:         10617        1722664  [C
3:        104057        1664912  com.miracle.current.lock.StackAllocationTest$User

我們可以看到程式總共建立了104057個User對象,遠小於100萬。我們可以關閉逃逸分析再來看下:

-Xmx2G -Xms2G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

啟動程序,透過jmap工具查看實例數:

jmap -histo 42928

 num     #instances         #bytes  class name
----------------------------------------------
   1:           628       22299176  [I
   2:       1000000       16000000  com.miracle.current.lock.StackAllocationTest$User

可以看到,關閉逃逸分析後總共創建了100萬個User物件。比較來看,堆疊上分配對堆疊記憶體消耗,GC都有著重要的作用。

標量替換

如果一個資料已經無法再分解成更小的資料來表示了,Java虛擬機器中的原始資料類型(int 、long 等數值型別及reference型別等)都不能再進一步分解了,那麼這些資料就可以稱為標量。相對的,如果一個資料可以繼續分解,那麼它就被稱為聚合量(Aggregate),Java中的物件就是典型的聚合量。

假如逃逸分析能夠證明一個對像不會被方法外部訪問,並且這個對象可以被拆散,那麼程序真正執行的時候將可能不去創建這個對象,而改為直接創建它的若干個被這個方法使用的成員變數來代替。

有以下程式碼:

public static void main(String[] args) {

    method();
}

private static void method() {
    User user = new User(25);

    System.out.println(user.age);
}

private static class User {

    private int age;

    public User(int age) {
        this.age = age;
    }
}

method()方法中创建User对象,指定age为25,这里User不会被其他方法引用,也就是说它不会逃逸出方法,并且User是可以拆解为标量的。所以alloc()代码会优化为如下:

private static void alloc() {
    int age = 25;

    System.out.println(age);
}

总结

尽管目前逃逸分析技术仍在发展之中,未完全成熟,但它是即时编译器优化技术的一个重要前进方向,在日后的Java虚拟机中,逃逸分析技术肯定会支撑起一系列更实用、有效的优化技术。

以上是3分鐘搞清楚 JVM逃逸分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:Java后端技术全栈。如有侵權,請聯絡admin@php.cn刪除