對象並不必定都是在堆上分配內存的

在《深刻理解 Java 虛擬機》中有這樣一段話:
「隨着 JIT 編譯器的發展和逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化,全部的對象分配到堆上也漸漸不那麼絕對了」。
緩存

逃逸分析

在編譯期間,JIT 會對代碼作不少優化,其中有一部分優化的目的就是減小內存堆分配的壓力,其中一項重要的技術叫作逃逸分析。bash

  • 方法逃逸
    逃逸分析基本行爲就是分析對象動態做用域:當一個對象在方法中被定義後,它可能會被外部方法調用,例如做爲調用參數傳遞到其餘方法中,稱爲方法逃逸。多線程

  • 線程逃逸
    甚至還有可能被外部線程訪問到,譬如複製給類變量或者在其餘線程中訪問的實例變量。成爲線程逃逸。性能

  • 目的
    經過逃逸分析,HotSpot 編譯器可以分析出一個新的對象的引用的使用範圍,從而決定是否要將這個對象分配到堆上。優化

  • 開啓、關閉與查看
    JDK 6 Update 23 版本以後,HotSpot 中默認就開啓了逃逸分析。
    開啓: -XX: +DoEscapeAnalysis (只能在 server 模式下開啓:-server)
    關閉: -XX: -DoEscapeAnalysis
    查看分析結果:-XX: +PrintEscapeAnalysisspa

優化手段

若是能證實一個對象不會逃逸到方法或線程以外,也就是別的方法或線程沒法經過任何途徑訪問到這個對象,則可能爲這個變量進行一些高效優化,以下所示:.net

1. 棧上分配(Stack Alloction)

  • JVM中,在Java堆上分配建立對象的內存空間。Java堆中的對象對於各個線程都是共享和可見的,只要持有這個對象的引用,就能夠訪問堆中存儲的對象數據。線程

  • JVM中垃圾收系統能夠回收堆中再也不使用的對象,但回收動做不管是篩選可回收對象,仍是回收和整理內存都須要耗費時間。3d

  • 若是肯定一個對象不會逃逸出方法以外,那讓這個對象在棧上分配內存將會是一個不錯的主意,對象所佔用的內存空間就能夠隨棧幀出棧而銷燬。在通常應用中,不會逃逸的局部變量所佔的比例很大,若是能使用棧上分配,那大量的對象就會隨着方法的結束而自動銷燬了,垃圾收集系統的壓力將會小不少。code

    TLAB 上分配

    • Thread Local Allocation Buffer,線程本地分配緩存。
    • 爲了加速對象分配。因爲對象通常在堆上,而堆是共享的,須要同步。
    • 佔用 eden 區空間,在 TLAB 啓用狀況下,虛擬機會爲每個線程分配一塊 TLAB 空間。默認很小(2048)。
    • 開啓/ 查看: -XX: +UserTLAB / -XX: +PrintTLAB
    • -XX:TLABSize 指定大小
    • -XX:-ResizeTLAB 大小會一直調整,能夠禁止調整,一次性設置值。
    • 對象分配流程圖

    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 區仍然不足以存放當前對象,則直接分配到老年代。
    複製代碼

2. 同步消除(Synchronization Elimination)

  • 線程同步
    圖11-2描述了兩個線程讀寫相同變量的假設例子。
    在這個例子中,線程 A 讀取變量而後給這個變量賦予一個新的值,但寫操做須要兩個存儲器週期。
    當線程 B 在這兩個存儲器寫週期中間讀取這個相同的變量時,它就會獲得不一致的值。
    
    爲了解決這個問題,線程不得不使用鎖,在同一時間只容許一個線程訪問該變量。
    
    圖 11-3 描述了這種同步。
    若是線程 B 但願讀取變量,它首先要獲取鎖;
    一樣地,當線程 A 更新變量時,也須要獲取這把一樣的鎖。
    於是線程 B 在線程 A 釋放鎖之前不能讀取變量。
    複製代碼
  • 線程同步自己是一個相對耗時的過程,若是逃逸分析可以肯定一個變量不會逃逸出線程,沒法被其餘線程訪問,那這個變量的讀寫確定就不會有競爭,對這個變量實施的同步措施也就能夠消除掉。
  • 開啓:-XX: +EliminateAllocations
  • 查看標量的替換狀況:-XX: +PrintEliminateAllocations

3.標量替換(Scalar Replacement)

  • 標量(Scalar)
    是一個數據已經沒法再分解成更小的數據來表示了,JVM中的原始數據類型(int、long等數值類型以及reference類型等)都不能進一步分解,他們就能夠成爲標量。
  • 聚合量(Aggregation)
    相對的,若是一個數據能夠繼續分解,那它就稱爲聚合量(Aggregation),Java中的對象就是最典型的聚合量。
  • 標量替換
    若是把一個Java對象拆解,根據程序訪問的狀況,將其使用到的成員變量恢復原始類型來訪問就叫作標量替換。
  • 條件
    若是逃逸分析證實一個對象不會被外部訪問,而且這個對象能夠被拆解的話,那程序真正執行的時候將可能不在建立這個對象,而改成直接建立它的若干個被這個方法使用到的成員變量來代替。
  • 優勢
    將對象拆分後,除了可讓對象的成員變量在棧上(棧上存儲的數據,有很大的機率會被JVM分配至物理機的告訴寄存器中存儲)分配合讀寫以外,還能夠爲後續進一步優化手段建立條件。
  • 開啓: -XX: +EliminateLocks
  • 關閉: -XX: -EliminateLocks

並不成熟

關於逃逸分析的論文在 1999 年就已經發表了,但直到 JDK 1.6 纔有實現,並且這項技術到現在也並非十分紅熟的。

在很長的一段時間裏,即便是Server Compiler,也默認不開啓逃逸分析,甚至在某些版本(如 JDK 1.6 Update 18)中還曾經短暫地徹底禁止了這項優化。

其根本緣由就是沒法保證逃逸分析的性能消耗必定能高於他的消耗。雖然通過逃逸分析能夠作標量替換、棧上分配、和鎖消除。可是逃逸分析自身也是須要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。

一個極端的例子,就是通過逃逸分析以後,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。

雖然這項技術並不十分紅熟,可是他也是即時編譯器優化技術中一個十分重要的手段,從性能分析中來看,使用逃逸分析的優化仍是頗有必要的。



參考來源:
周志明 《深刻理解Java虛擬機》
Java 中的逃逸分析和 TLAB 以及 Java 對象分配
關於棧上分配和 TLAB 的理解

相關文章
相關標籤/搜索