在《深刻理解 Java 虛擬機》中有這樣一段話:
「隨着 JIT 編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化,全部的對象分配到堆上也漸漸不那麼絕對了」。緩存
在編譯期間,JIT 會對代碼作不少優化,其中有一部分優化的目的就是減小內存堆分配的壓力,其中一項重要的技術叫作逃逸分析。bash
方法逃逸
逃逸分析基本行爲就是分析對象動態做用域:當一個對象在方法中被定義後,它可能會被外部方法調用,例如做爲調用參數傳遞到其餘方法中,稱爲方法逃逸。多線程
線程逃逸
甚至還有可能被外部線程訪問到,譬如複製給類變量或者在其餘線程中訪問的實例變量。成爲線程逃逸。性能
目的
經過逃逸分析,HotSpot 編譯器可以分析出一個新的對象的引用的使用範圍,從而決定是否要將這個對象分配到堆上。優化
開啓、關閉與查看
在 JDK 6 Update 23
版本以後,HotSpot 中默認就開啓了逃逸分析。
開啓: -XX: +DoEscapeAnalysis (只能在 server 模式下開啓:-server)
關閉: -XX: -DoEscapeAnalysis
查看分析結果:-XX: +PrintEscapeAnalysisspa
若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何途徑訪問到這個對象,則可能爲這個變量進行一些高效優化,以下所示:.net
JVM中,在Java堆上分配建立對象的內存空間。Java堆中的對象對於各個線程都是共享和可見的,只要持有這個對象的引用,就能夠訪問堆中存儲的對象數據。線程
JVM中垃圾收系統能夠回收堆中再也不使用的對象,但回收動做不管是篩選可回收對象,仍是回收和整理內存都須要耗費時間。3d
若是肯定一個對象不會逃逸出方法以外,那讓這個對象在棧上分配內存將會是一個不錯的主意,對象所佔用的內存空間就能夠隨棧幀出棧而銷燬。在通常應用中,不會逃逸的局部變量所佔的比例很大,若是能使用棧上分配,那大量的對象就會隨着方法的結束而自動銷燬了,垃圾收集系統的壓力將會小不少。code
1. 編譯器經過逃逸分析,肯定對象是在棧上分配仍是在堆上分配。若是是在堆上分配,則進入選項
2.若是 tlab_top + size <= tlab_end,則在在 TLAB 上直接分配對象並增長 tlab_top 的值,若是現有的 TLAB 不足以存放當前對象則
3.從新申請一個 TLAB,並再次嘗試存放當前對象。若是放不下,則 4
4.在 Eden 區加鎖(這個區是多線程共享的),若是 eden_top + size <= eden_end 則將對象存放在 Eden 區,增長 eden_top 的值,若是 Eden 區不足以存放,則 5
5.執行一次 Young GC(minor collection)。
6. 通過 Young GC 以後,若是 Eden 區仍然不足以存放當前對象,則直接分配到老年代。
複製代碼
圖11-2描述了兩個線程讀寫相同變量的假設例子。
在這個例子中,線程 A 讀取變量而後給這個變量賦予一個新的值,但寫操做須要兩個存儲器週期。
當線程 B 在這兩個存儲器寫週期中間讀取這個相同的變量時,它就會獲得不一致的值。
爲了解決這個問題,線程不得不使用鎖,在同一時間只容許一個線程訪問該變量。
圖 11-3 描述了這種同步。
若是線程 B 但願讀取變量,它首先要獲取鎖;
一樣地,當線程 A 更新變量時,也須要獲取這把一樣的鎖。
於是線程 B 在線程 A 釋放鎖之前不能讀取變量。
複製代碼
關於逃逸分析的論文在 1999 年就已經發表了,但直到 JDK 1.6 纔有實現,並且這項技術到現在也並非十分紅熟的。
在很長的一段時間裏,即便是Server Compiler,也默認不開啓逃逸分析,甚至在某些版本(如 JDK 1.6 Update 18
)中還曾經短暫地徹底禁止了這項優化。
其根本緣由就是沒法保證逃逸分析的性能消耗必定能高於他的消耗。雖然通過逃逸分析能夠作標量替換、棧上分配、和鎖消除。可是逃逸分析自身也是須要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。
一個極端的例子,就是通過逃逸分析以後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
雖然這項技術並不十分紅熟,可是他也是即時編譯器優化技術中一個十分重要的手段,從性能分析中來看,使用逃逸分析的優化仍是頗有必要的。
參考來源:
周志明 《深刻理解Java虛擬機》
Java 中的逃逸分析和 TLAB 以及 Java 對象分配
關於棧上分配和 TLAB 的理解