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