面試官:Java中對象都存放在堆中嗎?你知道逃逸分析?

  • 2022 年 3 月 14 日
  • 筆記

面試官:Java虛擬機的記憶體分為哪幾個區域?

我(微笑著):程式計數器、虛擬機棧、本地方法棧、堆、方法區

面試官:對象一般存放在哪個區域?

我:堆。

面試官:對象都存放在堆中嗎?

我:是的。

面試官:你了解過逃逸分析嗎?

我(皺了皺眉):是記憶體溢出嗎?

面試官:不是的。

我(撓了撓頭):不是很了解。

面試官:今天的面試先到這,回去等消息吧!

然後就沒有然後了,不甘心的我開始了查找相關資料。

逃逸分析

逃逸分析(Escape Analysis)是一種確定對象的引用動態範圍的分析方法,說人話就是:分析在程式的哪些地方可以訪問到對象的引用。

當一個對象在方法中被分配時,該對象的引用可能逃逸到其它執行執行緒中,或是返回到方法的調用者。

如果一個方法中分配一個對象並返回一個該對象的引用針,那麼該對象可能被訪問到的地方就無法確定,此時對象的引用就發生了「逃逸」。
如果對象的引用存儲在靜態變數或者其它數據結構中,因為靜態變數是可以在當前方法之外訪問到,此時對象的引用也發生了「逃逸」。

逃逸分析確定某個對象的引用可以被訪問的所有地方,以及確定能否保證對象的引用的生命周期只在當前進程或執行緒中。

逃逸狀態

對象的逃逸狀態一般分為三種:全局逃逸、參數逃逸、沒有逃逸。

全局逃逸(GlobalEscape)

對象的引用逃出了方法或者執行緒。比如:對象的引用賦值給了一個靜態變數,或者存儲在一個已經逃逸的對象中, 或者對象的引用作為方法的返回值給了調用方法。

比如餓漢的單例模式:

package one.more;

public final class GlobalEscape {

    // instance對象賦值給了一個靜態變數,發生了全局逃逸
    private static GlobalEscape instance = new GlobalEscape();

    private GlobalEscape() {
    }

    public static GlobalEscape getInstance() {
        return instance;
    }
}

參數逃逸(ArgEscape)

對象被作為方法參數傳遞或者被參數引用,但在調用過程中不會發生全局逃逸。這個狀態是通過分析被調用方法的位元組碼來確定的。

比如:

package one.more;

public class ArgEscape {

    class Rectangle {

        private int length;
        private int width;

        public Rectangle(int length, int width) {
            this.length = length;
            this.width = width;
        }

        public int getArea() {
            return this.length * this.width;
        }
    }

    public int getArea(int length, int width) {
        Rectangle rectangle = buildRectangle(length, width);
        return rectangle.getArea();
    }

    private Rectangle buildRectangle(int length, int width){
        Rectangle rectangle = new Rectangle(length, width);
        // rectangle對象發生了參數逃逸
        return rectangle;
    }
}

沒有逃逸(NoEscape)

方法中的對象沒有發生逃逸,這意味著可以不將該對象分配在堆上。

比如:

package one.more;

public class NoEscape {

    class Rectangle {

        private int length;
        private int width;

        public Rectangle(int length, int width) {
            this.length = length;
            this.width = width;
        }

        public int getArea() {
            return this.length * this.width;
        }
    }

    public int getArea(int length, int width) {
        // rectangle對象沒有逃逸
        Rectangle rectangle = new Rectangle(length, width);
        return rectangle.getArea();
    }
}

逃逸分析後的優化

如果一個對象沒有發生逃逸,或者只有參數逃逸,就可能為這個對象採取不同程度的優化,比如:棧上分配、標量替換、同步消除。

棧上分配(Stack Allocations)

如果一個對象不會逃逸出執行緒之外,那讓這個對象在棧上分配記憶體將會是一個很不錯的主意,對象所佔用的記憶體空間就可以隨棧幀出棧而銷毀。
那麼,對象就會隨著方法的結束而自動銷毀了,可以降低垃圾收集器運行的頻率,垃圾收集的壓力就會下降很多。

標量替換(Scalar Replacement)

標量(Scalar)是指一個無法再分解成更小的數據的數據。Java虛擬機中的基本數據類型(int、long等數值類型及reference類型等)都不能再進一步分解了,那麼這些數據就可以被稱為標量。相對的,如果一個數據可以繼續分解,那它就被稱為聚合量(Aggregate),Java中的對象就是典型的聚合量。

如果把一個Java對象拆散,根據程式訪問的情況,將其用到的成員變數恢復為基本類型來訪問,這個過程就稱為標量替換

如果一個對象沒有發生逃逸,可以進行標量替換,那麼對象的成員變數就在棧上分配和讀寫,不需要分配到堆中。

標量替換可以視作棧上分配的一種特例,實現更簡單,但對逃逸程度的要求更高,它不允許對象沒有發生逃逸。

同步消除(Synchronization Elimination)

執行緒同步本身是一個相對耗時的過程,如果一個對象沒有逃逸出執行緒,無法被其他執行緒訪問,那麼該對象的讀寫肯定就不會有競爭,對該對象實施的同步加鎖操作也就可以安全地消除掉。

總結

說了這麼多,可以發現對象並不是都在堆上分配記憶體的。因為通過逃逸分析後,可以對沒有逃逸的對象進行標量替換。

另外,由於複雜度等原因,HotSpot中目前還不支援棧上分配的優化。

最後,謝謝你這麼帥,還給我點贊關注

微信公眾號:萬貓學社

微信掃描二維碼

關注後回復「電子書」

獲取12本Java必讀技術書籍