JVM垃圾回收

垃圾回收與內存分配策略

「垃圾」的定義

對象是否爲「垃圾」

判斷對象是否已成爲「垃圾」的兩種方法:引用計數法可達性分析算法java

  • 引用計數法

若是一個對象被引用一次,則加1,若是沒人引用則被回收;存在問題:若是兩個對象循環引用,可是沒有任何外部對象引用他們倆,則那兩個對象沒法被回收。算法

  • 可達性分析算法(主流JVM採用)

沒有被根對象(GC ROOT)直接或簡介引用的對象則會被回收
根對象--確定不能對回收的對象
GC ROOT對象:system class、同步鎖、線程類、本地方法類數據結構

何爲「引用」--四種引用類型

JDK1.2之後將引用分爲:強引用、軟引用、弱引用和虛引用4種,強度依次減弱。多線程

  • 強引用
    被GC ROOT直接引用(等號賦值
  • 軟引用
    被GC ROOT間接引用;當內存不足時被回收,內存充足時不會被回收
  • 弱引用
    沒有GC ROOT直接引用,當發生垃圾回收時,無論內存是否充足都會被回收
  • 虛引用
    沒有GC ROOT直接引用,虛引用使用時必須配合引用隊列進行管理。

好比建立一個ByteBuffer實現類對象時,會建立個一個Cleaner對象,當ByteBuffer實現類對象沒有再被引用時,ByteBuffer實現類對象會被回收,Cleaner對象則會進入引用隊列,這時候一個referencehandles線程會查找引用隊列中是否存在cleaner對象,若是有則調用Cleaner.clean方法,clean方法則根據記錄的直接內存的地址,調用unsafe.freememory方法釋放直接內存併發

  • 補充:引用隊列

軟引用、弱引用自己也要佔用必定內存,當軟引用、弱引用的引用對象都被回收時,則進入引用隊列,會對引用隊列進行後續管理;虛引用引用的對象被釋放後,虛引用會進入引用隊列異步

最後的掙扎--finalize()方法

即便可達性分析後,對象被斷定爲「垃圾」,也並不是非死不可。一個對象的死亡至少須要兩次標記:函數

沒有與GC Root的引用鏈,標記一次
對象沒有重寫finalize()方法,或finalize()重寫但已被調用過一次,標記第二次佈局

若是重寫了finalize()方法,且尚未被調用,那麼對象會被放置在F-Queue的隊列中,會有一條虛擬機自建的、優先度較低的線程Finalizer線程去執行對象的finalize()方法,但爲了防止finalize()方法出現死循環等異常,並不會保證等待finalize()方法執行結束。在此期間,若對象創建了引用鏈,則對象能夠存活一次,不然就「死定了」。線程

不建議使用該finalize()方法設計

回收方法區

方法區的垃圾回收主要包含兩部分:廢棄的常量、再也不使用的類型

常量的回收相似與Java堆中的對象,當沒有引用時,則容許回收
類型的回收相對比較苛刻,須要同時知足如下條件,才容許被回收

  • 該類全部實例都已被回收
  • 該類的類加載器已被回收
  • 該類對應的java.lang.Class對象沒有被引用,且在任何地方都不能夠經過反射訪問該類方法

垃圾回收算法

從斷定垃圾消亡的角度出發,垃圾回收算法能夠劃分爲「引用計數式垃圾收集」、「追蹤式垃圾收集」兩類。在Java虛擬機中的討論都在追蹤式垃圾收集的範疇中。

回收的前置--分代理論

分代設計的理論創建在兩個分代假說之上:

  1. 弱分代假說:新生對象都是朝生夕死
  2. 強分代假說:熬過越屢次垃圾回收的對象,就越難以消亡

    設計原則:

    垃圾收集器應該依據對象的年齡,把Java堆劃分爲不一樣的區域。

    • 新生代
      朝生夕滅的對象集中在一個區域,每次回收只需關注少許須要存活的對象便可
    • 老年代
      難以消亡的對象集中在一個區域,可使用較低的頻率去觸發回收機制

    可是,在對新生代進行垃圾收集的時候,難免會出現新生代的中的對象被老年代引用的狀況。因此,爲了肯定新生代區域的存活對象,除了GC Root以外還須要遍歷整個老年代中全部對象來得到準確的可達性分析。基於此,引入第三條經驗法則:

  3. 跨代引用假說:跨代引用相對於同代引用來講只佔少數

    跨代引用通常傾向於兩個對象同時生存或同時消亡的

    設計原則:

    在新生代創建全局數據結構(記憶集),把老年代分爲若干小塊,記錄老年代中哪一塊內存存在跨代引用
    此後,發生minor gc時只有包含了跨代引用的小塊內存中的對象纔會被加入到GC Root進行掃描

標記-清除算法(Mark Sweep)

先標記須要回收的對象,再統一清除

效率不穩定,隨着對象數量增多,標記、清除兩個過程的執行效率下降
內存碎片化,致使存入大對象時沒法得到足夠的連續內存空間,觸發另外一次垃圾收集動做

標記-複製算法

將可用內存劃分爲兩個徹底相等空間,每次只使用其中的一塊。若是其中的一塊內存用完,則將存活的對象徹底複製到另外一塊,再對原來的空間進行統一清除回收。

  • 缺點
    內存空間的浪費
    若空間內大量對象都是存活的,複製的開銷增大
  • 優勢
    簡單高效
    不用考慮內存空間碎片化
    PS.
    現商用Java虛擬機多在新生代中採用該方法
  • Appel式回收
    HotSpot虛擬機中的Serial、ParNew等新生代收集器均採起該策略。具體以下:
    把新生代分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配只使用Eden和一塊Survivor,發生垃圾回收時,將存活的對象一次性複製給另外一塊Survivor空間內,而後清理已用的空間。

    HotSpot虛擬機給Eden和Survivor默認大小比例爲8:1,也就是說會有10%的空間會被浪費。當預留的10%的內存空間存不下存活的對象時,,就須要依賴其它內存空間(大多爲老年代)進行內存分配。

標記-整理算法(Mark Compact)

區別與標記--清除算法,標記--整理算法,在標記後將存活的對象移向一端,而後將另外一端的空間總體回收,是一種移動式的算法。

  • 優勢
    不存在碎片化內存,則無需依賴複雜的內存分配器
  • 缺點
    對象的移動操做須要觸發「Stop The World」耗時較久

標記?清除:整理

標記-清除是一種非移動式算法、標記-整理是一種移動式算法,二者比較說明:

  • 吞吐量比較
    吞吐量定義:賦值器和收集器效率之和
    不移動會使得收集器效率增大,可是內存分配和訪問會比垃圾回收頻率高得多,因此總體吞吐量仍是下降的。

  • 舉例說明
    HotSpot虛擬機中關注吞吐量的Parallel Scavenger收集器基於標記-整理算法;關注低延遲的CMS收集器基於標記-清除算法

  • 混合方案
    使虛擬機多數時間採用標記-清除算法,暫時容忍碎片的存在,等到碎片化程度開始影響對象的內存分配時,在採用標記-整理算法收集一次(CMS就採起該方式)

經典垃圾回收器

所謂「經典」垃圾回收器是指區別於實驗室階段的、已經過應用實踐的垃圾回收器。

HotSpot垃圾回收器

Serial收集器

Serial:新生代:標記-複製算法
Serial Old:老年代:標記-整理算法
HotSpot虛擬機運行在客戶端模式下的默認新生代收集器
簡單高效、內存消耗最小

ParNew收集器

ParNew:新生代:標記-複製算法
Serial Old:老年代:標記-整理算法
激活CMS後,默認的新生代收集器
Serial的多線程版本,默認開啓的線程數與CPU核心數相同

Parallel Scavenge蒐集器

標記-複製算法,與ParNew類似
關注點在於達成可控制的吞吐量(吞吐量=用戶代碼運行時間/總時間;總時間=用戶代碼運行時間+垃圾回收時間)

參數說明

  • -XXMaxGCPauseMillis更關注停頓時間
    一個大於0的毫秒數,儘可能使回收時間不超過這個值
    實現原理:犧牲吞吐量和新生代空間獲取,小內存新生代空間的回收速度必定因爲高內存速度,可是回收頻率也會增長

  • -XXGCTimeRatio更關注吞吐量
    0到100之間的整數,表明垃圾回收時間佔總時間的比率,至關於吞吐量的倒數

  • -UserAdaptiveSizePolicy
    開關函數,激活後虛擬機會根據當前運行狀況自動調整Eden與Survivor的內存比例、老年代內存大小等參數,已提供合適的停頓時間和最大吞吐量

Serial Old 收集器

serial 收集器的老年版本,標記-整理算法
在CMS收集器併發失敗時的預備方案

Parallel Old 收集器

Parallel Scavenge 收集器的老年版本,標記-整理算法
在注重吞吐量或處理器資源稀缺時使用

CMS收集器

獲取最短停頓時間的爲目標,採用併發-清除算法

  • 工做步驟
    1. 初始標記
      標記GC Roots能直接關聯的對象,速度很快

    2. 併發標記
      從GC Roots直接關聯到的對象開始遍歷整個對象圖,耗時較長

    3. 從新標記
      修正併發標記期間,因用戶繼續運做致使標記產生變更的部分對象的標記記錄

    4. 併發清除
      清除掉標記的已死亡的對象

整個過程當中,併發標記和併發清除耗時最久

  • 關鍵問題
    1. 併發過程當中會佔用部分資源
      當處理器核心數大於4時,默認回收線程數不超過25%(處理器核心數+3)/4
      可是當處理器核心數小於4時,用戶線程執行速度會大幅下降

    2. 「浮動垃圾」與併發失敗
      與用戶程序運行併發運行就必然產生新的垃圾只有等下一次回收時才清理,這部分垃圾稱爲「浮動垃圾」,因此須要給用戶線程預留足夠空間。所以,CMS不能等老年代滿了才進行收集,必須預留一部分做爲併發時使用。若是CMS運行期間預留的內存沒法知足程序分配新對象的需求,就會出現「併發失敗」,這時候須要STW,臨時啓用Serial Old收集器對老年代的垃圾進行收集

    3. 內存碎片
      基於標記-清除算法必然產生內存碎片,致使大對象分配時出現內存不足進而觸發Full GC。CMS提供-XX:UseCMSCompactAtFullCollection開關參數(默認開啓),當不得不進行Full GC時進行內存碎片整合,即移動存活對象。會使得停頓時間延長

Garbage First 收集器

創建可預測的停頓時間模型,開創了面向局部收集的內存設計思路,基於Region的內存佈局形式。默認停頓時間爲200毫秒

  • 基於Region的內存佈局
    把連續的Java堆內存劃分爲多個大小相等的獨立空間,每一個空間均可以扮演Eden、Survivor空間或者老年代空間,其中Humongous區域轉爲收集大對象(大小超過了一個Region的對象,Region的大小可經過參數調整),G1大多會把Humongous當作老年代看待。收集器能夠根據不一樣的角色採起不一樣的收集策略。

  • 局部收集思想
    Region做爲每次回收的最小內存單位,每次收集到的空間都是Region的整倍數,G1會跟蹤Region堆積的「價值」大小(回收所獲空間/回收所需時間的經驗值),再後臺維護一個優先級列表,優先回收價值大的Region

  • 工做步驟
    1. 初始標記
      標記GC Roots能直接關聯的對象,並修改TAMS指針的值,是藉助Minor GC完成,因此不會形成額外的時間成本。
    2. 併發標記
      從GC Roots開始對堆中對象進行可達性分析,可併發執行,掃描完成時從新處理SATB記錄的引用變更
    3. 最終標記
      處理併發標記時的發生變更的對象,STW,併發完成
    4. 篩選回收
      更新Region的統計數據,根據用戶指望的停頓時間結合回收價值,肯定須要回收Region集合。把須要回收的Region中存活的對象複製到空Region中,再清理須要回收的所有Region區域。
  • 關鍵問題
    1. 跨Region引用的處理辦法
      每一個Region都維護一張本身的記憶集,記錄別的Region指向本身的指針,並標記這些指針在哪些卡頁範圍以內。其存儲結構本質上是一種哈希表,key是別的Region的起始地址,value是一個集合,存儲卡表的索引號。G1要耗費大越10%到20%的額外內存來維持收集器的工做。

    2. 併發干擾問題
      CMS在併發標記時採用增量更新的算法實現,而G1則經過原始快照(SATB)算法實現。此外,G1在回收過程當中建立新對象的內存分配上也作了改動,G1爲每一個Region設計了兩個名爲TAMS(Top At Mark Start)的指針,併發標記中新分配的對象都要在這兩個指針位置以上。G1收集器默認這部分對象是隱式標記過的,默認爲存活

    3. 可靠地停頓預測
      -XX:MaxGCPauseMillis參數指用戶指望的停頓時間,具體實現是以「衰減均值」爲理論基礎:在垃圾回收過程當中,會記錄每一個Region的回收耗時、記憶集中裏的髒卡數量等各個可測量的步驟所花費的成本。「衰減均值」更能體現「最近」一段時間的平均狀態,更能在當下使回收不超過預期。(有點活在當下的感受)

  • G1與CMS
    • 優勢:
      能夠指定最大停頓時間、分Region的內存佈局、按收益動態回收、不會產生內存碎片、回收完成後可提供規整的可用內存
    • 缺點:
      內存佔用、程序執行的額外負載都較高
      G1的卡表更爲複雜;運行負載方面,CMS使用寫後屏障來更細維護卡表,而G1爲了實現原始搜索(SATB)快照算法,還須要寫前屏障來跟蹤併發時的指針變化狀況,G1能減小併發標記和從新標記的消耗,避免像CMS那樣在最終標記階段停頓時間過長。CMS直接同步處理,而G1異步處理
  • 總結
    小內存上使用CMS有優點,而大內存狀態下使用G1有更多優點,而Java堆內存容量平衡點大約在6-8GB之間(經驗數據)

低延遲垃圾收集器

Shenandoah 收集器

ZGC 收集器

相關文章
相關標籤/搜索