要點提煉| 理解JVM之GC&記憶體分配

釐米姑娘發表於2018-07-08

有記憶體分配就會有記憶體回收,上篇也瞭解到Java堆是垃圾收集器管理的主要區域,本篇將理解這部分記憶體的垃圾回收機制。

  • 物件存活判定演算法
  • 垃圾收集演算法
  • HotSpot演算法實現&垃圾收集器
  • 記憶體分配與回收策略

1.物件存活判定演算法

概念:引用的四種型別

  • 強引用(StrongReference)
    • 具有強引用的物件不會被GC;
    • 即便記憶體空間不足,JVM寧願丟擲OutOfMemoryError使程式異常終止,也不會隨意回收具有強引用的物件。
  • 軟引用(SoftReference)
    • 只具有軟引用的物件,會在記憶體空間不足的時候被GC;
    • 軟引用常用來實現記憶體敏感的高速快取
  • 弱引用(WeakReference)
    • 只被弱引用關聯的物件,無論當前記憶體是否足夠都會被GC;
    • 強度比軟引用更弱,常用於描述非必需物件。
  • 虛引用(PhantomReference)
    • 僅持有虛引用的物件,在任何時候都可能被GC;
    • 常用於跟蹤物件被GC回收的活動;
    • 必須和引用佇列 (ReferenceQueue)聯合使用,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。

a.引用計數演算法:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的。

然而在主流的Java虛擬機器裡未選用引用計數演算法來管理記憶體,主要原因是它難以解決物件之間相互迴圈引用的問題,所以出現了另一種物件存活判定演算法。

b.可達性分析法:通過一系列被稱為『GC Roots』的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

可作為GC Roots的物件:

  • 虛擬機器棧中引用的物件,主要是指棧幀中的本地變數
  • 本地方法棧中Native方法引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件

要點提煉| 理解JVM之GC&記憶體分配

需要注意的是,在可達性分析演算法中被判定不可達的物件還未真的判『死刑』,GC還會在執行finalize()方法的物件中進行一次篩選,如果物件能在finalize()中重新與引用鏈上的任何一個物件建立關聯,將被移除出“即將回收”的集合。


引伸:有關方法區的GC,可分成兩部分

  • 廢棄常量與回收Java堆中的物件的GC很類似,即在任何地方都未被引用的常量會被GC。
  • 無用的類需滿足以下三個條件才會被GC:
    • 該類所有的例項都已被回收,即Java堆中不存在該類的任何例項;
    • 載入該類的ClassLoader已經被回收;
    • 該類對應的java.lang.Class物件沒在任何地方被引用,即無法在任何地方通過反射訪問該類的方法。

2.垃圾收集演算法

上一節介紹了JVM會回收哪些物件,接下來介紹JVM會如何回收掉這些物件。

a.分代收集演算法

  • 根據物件存活週期的不同,將Java堆劃分為新生代和老年代,並根據各個年代的特點採用最適當的收集演算法。
    • 新生代:大批物件死去,只有少量存活。使用『複製演算法』,只需複製少量存活物件即可。
    • 老年代:物件存活率高。使用『標記—清理演算法』或者『標記—整理演算法』,只需標記較少的回收物件即可。
  • 是當前商業虛擬機器都採用的一種演算法。

接下來依次介紹以上提及的另外三種演算法。

b.複製演算法

  • 把可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用盡後,把還存活著的物件『複製』到另外一塊上面,再將這一塊記憶體空間一次清理掉。
  • 優點:每次都是對整個半區進行記憶體回收,無需考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。
  • 缺點:每次可使用的記憶體縮小為原來的一半,記憶體使用率低。

要點提煉| 理解JVM之GC&記憶體分配

c.標記-清除演算法

  • 首先『標記』出所有需要回收的物件,然後統一『清除』所有被標記的物件。
  • 是最基礎的收集演算法。
  • 缺點:『標記』和『清除』過程的效率不高;空間碎片太多,『標記』『清除』之後會產生大量不連續的記憶體碎片,可能會導致後續需要分配較大物件時,因無法找到足夠的連續記憶體而提前觸發另一次GC,影響系統效能。

要點提煉| 理解JVM之GC&記憶體分配

d.標記-整理演算法

  • 首先『標記』出所有需要回收的物件,然後進行『整理』,使得存活的物件都向一端移動,最後直接清理掉端邊界以外的記憶體。
  • 優點:即沒有浪費50%的空間,又不存在空間碎片問題,價效比較高。
  • 一般情況下,老年代會選擇標記-整理演算法。

要點提煉| 理解JVM之GC&記憶體分配


3.HotSpot演算法實現&垃圾回收器

接下來介紹如何在HotSpot虛擬機器上實現物件存活判定演算法和垃圾收集演算法,並保證虛擬機器高效執行。

a.列舉根節點

主流Java虛擬機器使用的都是準確式GC,在執行系統停頓之後無需檢查所有執行上下文和全域性的引用位置,而是通過一些辦法直接獲取到存放物件引用的地方,在HotSpot中是通過一組稱為OopMap的資料結構來實現的,完成類載入後會計算出物件某偏移量上某型別資料、JIT編譯時會在特定的位置記錄棧和暫存器中是引用的位置。這樣GC在掃描時就可直接得知這些資訊,並快速準確地完成GC Roots的列舉。

b.安全點(Sefepoint)

上述“特定的位置”被稱為安全點,即程式執行時並非在所有地方都停頓執行GC,只在到達安全點時才暫停,降低GC的空間成本。

  • 安全點的選定標準:可讓程式長時間執行的地方,如方法呼叫、迴圈跳轉、異常跳轉等具有指令序列複用的特徵。
  • 使所有執行緒在最近的安全點上再停頓的方案:
    • 搶先式中斷(Preemptive Suspension):無需程式碼主動配合,在GC發生時把所有執行緒全部中斷,若執行緒中斷處不在安全點上就恢復執行緒,讓它“跑”到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒從而響應GC事件。
    • 主動式中斷(Voluntary Suspension):在GC要中斷執行緒時不直接對執行緒操作,而是設定一箇中斷標誌,讓各個執行緒在執行時主動輪詢它,當中斷標誌為真時就自己中斷掛起。

c.安全區域(Safe Region)

安全點機制只能保證程式執行時,在不太長的時間內遇到可進入GC的安全點,但在程式不執行時(如執行緒處於Sleep或Blocked狀態)執行緒無法響應JVM的中斷請求,此時就需要安全區域來解決。

  • 安全區域:引用關係不會發生變化的一段程式碼片段,在安全區域中的任意地方開始GC都是安全的,可看做是擴充套件的安全點。
  • 執行過程:當執行緒執行到安全區域中的程式碼時就標識一下,如果這時JVM要發起GC就不用管被標識的執行緒;線上程要離開安全區域時檢查系統是否已經完成了根節點列舉,若完成則執行緒可以繼續執行,否則等待直到收到可以安全離開安全區域的訊號為止。

到此只是簡單介紹了HotSpot如何發起記憶體回收,而具體的回收動作是由虛擬機器所採用的GC收集器決定的,通常虛擬機器中往往不止有一種GC收集器,下圖展示的是HotSpot虛擬機器中存在的七種作用於不同分代(新生代、老年代)的收集器,其中被連線的兩個收集器表示可以搭配使用。

要點提煉| 理解JVM之GC&記憶體分配

以下是對比圖,來源於文章JVM(HotSpot) 垃圾收集器

要點提煉| 理解JVM之GC&記憶體分配

並行(Parallel):多條垃圾收集執行緒並行工作,而使用者執行緒仍處於等待狀態。 併發(Concurrent):垃圾收集執行緒與使用者執行緒一段時間內同時工作,使用者程式在繼續執行,而垃圾收集程式執行於另一個CPU上。


5.記憶體分配與回收策略

物件的記憶體分配廣義上是指在堆上分配,主要是在新生代的Eden區上,如果啟動了TLAB,將按執行緒優先在TLAB上分配,少數情況下也可能會分配在老年代中。分配細節還是取決於所使用的GC收集器組合以及虛擬機器中與記憶體相關的引數的設定。以下介紹幾條普遍的記憶體分配規則。

  • 物件優先在Eden分配:大多數情況下物件在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時虛擬機器將發起一次Minor GC。

新生代GC(Minor GC):發生在新生代的垃圾收集動作。較頻繁、回收速度也較快。 老年代GC(Major GC/Full GC):發生在老年代的垃圾收集動作。出現Major GC經常會伴隨至少一次的Minor GC。速度一般比Minor GC慢10倍以上。

  • 大物件直接進入老年代:對於需要大量連續記憶體空間的Java物件(如很長的字串以及陣列),如果大於虛擬機器設定的-XX:PretenureSizeThreshold引數值將直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製。

  • 長期存活的物件將進入老年代:虛擬機器會給每個物件定義一個年齡計數器,當物件在Eden出生並經過第一次Minor GC後仍存活且能被Survivor容納的話,將被移動到Survivor空間中並將物件年齡設為1;當物件在Survivor區中每“熬過”一次Minor GC年齡就+1,直至增加到一定程度(預設為15歲,可通過-XX: MaxTenuringThreshold設定)就會被晉升到老年代中。

  • 動態物件年齡判定:為了能更好地適應不同程式的記憶體狀況,虛擬機器並不要求一定要達到-XX: MaxTenuringThreshold設定值才能晉升到老年代,當Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,那麼年齡大於或等於該年齡的物件可以直接進入老年代。

  • 空間分配擔保:在發生Minor GC之前虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,若是,說明可確保Minor GC是安全的,反之虛擬機器會檢視-XX:HandlePromotionFailure設定值是否允許擔保失敗;若允許,會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小;若大於,將嘗試進行一次Minor GC,若小於或者不允許擔保失敗,將改為進行一次Full GC。

解釋:當大量物件在MinorGC後仍然存活的情況時,需要藉助老年代進行分配擔保,把Survivor無法容納的物件直接進入老年代,但前提是老年代本身還有容納這些物件的剩餘空間,由於在完成記憶體回收之前無法預知實際存活物件,只好取之前每次回收晉升到老年代物件容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,從而決定是否進行Full GC來讓老年代騰出更多空間。

相關文章