物件並不一定都是在堆上分配記憶體的
在《深入理解 Java 虛擬機器》中有這樣一段話:
“隨著 JIT 編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的物件分配到堆上也漸漸不那麼絕對了”。
逃逸分析
- 這是由於在編譯期間,JIT 會對程式碼做很多優化,其中有一部分優化的目的就是減少記憶體堆分配的壓力,其中一項重要的技術叫做逃逸分析。
- 逃逸分析基本行為就是分析物件動態作用域:當一個物件在方法中被定義後,它可能會被外部方法呼叫,例如作為呼叫引數傳遞到其他方法中,稱為方法逃逸。
- 甚至還有可能被外部執行緒訪問到,譬如複製給類變數或者在其他執行緒中訪問的例項變數。成為執行緒逃逸。
- 通過逃逸分析,HotSpot 編譯器能夠分析出一個新的物件的引用的使用範圍,從而決定是否要將這個物件分配到堆上。
如果能證明一個物件不會逃逸到方法或執行緒之外,也就是別的方法或執行緒無法通過任何途徑訪問到這個物件,則可能為這個變數進行一些高效優化,如下所示:
棧上分配(Stack Alloction)
- JVM中,在Java堆上分配建立物件的記憶體空間。Java堆中的物件對於各個執行緒都是共享和可見的,只要持有這個物件的引用,就可以訪問堆中儲存的物件資料。
- JVM中垃圾收系統可以回收堆中不再使用的物件,但回收動作無論是篩選可回收物件,還是回收和整理記憶體都需要好費時間。
- 如果確定一個物件不會逃逸出方法之外,那讓這個物件在棧上分配記憶體將會是一個不錯的主意,物件所佔用的記憶體空間就可以隨棧幀出棧而銷燬。在一般應用中,不會逃逸的區域性變數所佔的比例很大,如果能使用棧上分配,那大量的物件就會隨著方法的結束而自動銷燬了,垃圾收集系統的壓力將會小很多。
同步消除(Synchronization Elimination)

圖11-2描述了兩個執行緒讀寫相同變數的假設例子。 在這個例子中,執行緒 A 讀取變數然後給這個變數賦予一個新的值,但寫操作需要兩個儲存器週期。 當執行緒 B 在這兩個儲存器寫週期中間讀取這個相同的變數時,它就會得到不一致的值。 為了解決這個問題,執行緒不得不使用鎖,在同一時間只允許一個執行緒訪問該變數。 圖 11-3 描述了這種同步。 如果執行緒 B 希望讀取變數,它首先要獲取鎖; 同樣地,當執行緒 A 更新變數時,也需要獲取這把同樣的鎖。 因而執行緒 B 線上程 A 釋放鎖以前不能讀取變數。 複製程式碼
- 執行緒同步本身是一個相對耗時的過程,如果逃逸分析能夠確定一個變數不會逃逸出執行緒,無法被其他執行緒訪問,那這個變數的讀寫肯定就不會有競爭,對這個變數實施的同步措施也就可以消除掉。
標量替換(Scalar Replacement)
- 標量(Scalar)是一個數據已經無法再分解成更小的資料來表示了,JVM中的原始資料型別(int、long等數值型別以及reference型別等)都不能進一步分解,他們就可以成為標量。
- 相對的,如果一個數據可以繼續分解,那它就稱為聚合量(Aggregation),Java中的物件就是最典型的聚合量。
- 如果把一個Java物件拆解,根據程式訪問的情況,將其使用到的成員變數恢復原始型別來訪問就叫做標量替換。
- 如果逃逸分析證明一個物件不會被外部訪問,並且這個物件可以被拆解的話,那程式真正執行的時候將可能不在建立這個物件,而改為直接建立它的若干個被這個方法使用到的成員變數來代替。
- 將物件拆分後,除了可以讓物件的成員變數在棧上(棧上儲存的資料,有很大的概率會被JVM分配至物理機的告訴暫存器中儲存)分配合讀寫之外,還可以為後續進一步優化手段建立條件。
並不成熟
關於逃逸分析的論文在 1999 年就已經發表了,但直到 JDK 1.6 才有實現,而且這項技術到如今也並不是十分成熟的。
其根本原因就是無法保證逃逸分析的效能消耗一定能高於他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
一個極端的例子,就是經過逃逸分析之後,發現沒有一個物件是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
雖然這項技術並不十分成熟,但是他也是即時編譯器優化技術中一個十分重要的手段。
參考來源:
周志明 《深入理解Java虛擬機器》