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

chouer523發表於2022-03-15

面試官: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必讀技術書籍

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

相關文章