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