JVM GC 原理

垃圾回收機制

分代回收理論

  • 新生代:絕大部分的對象都是朝生夕死
  • 老年代:熬過多次垃圾回收的對象就越難回收

GC 分類

  • 新生代回收(Minor GC/Young GC)
  • 老年代回收(Major GC/Old GC)
    • 只有 CMS 垃圾回收器會有這個單獨的回收老年代的行為
    • Major GC 有說指是老年代,有說是做整個堆的收集
  • 整堆回收(Full GC):收集整個 Java 堆和方法區(qū)(注意包含方法區(qū))

STW (Stop The World)

進行垃圾回收時,必須暫停所有的工作線程,直到它回收結(jié)束,這個暫停稱之為 STW,JVM 開發(fā)團隊一直努力消除或降低 STW 的時間。

垃圾回收算法

新生代

復制算法(Copying)- 1:1

將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。只是這種算法的代價是將內(nèi)存縮小為了原來的一半。
但是要注意:內(nèi)存移動是必須實打?qū)嵉囊苿樱◤椭疲?,所以對?yīng)的引用(直接指針)需要調(diào)整。

復制回收算法適合于新生代,因為大部分對象朝生夕死,那么復制過去的對象比較少,效率自然就高,另外一半的一次性清理是很快的。

復制算法 - Appel 式回收 - 8:1:1

一種更加優(yōu)化的復制回收分代策略:具體做法是分配一塊較大的Eden 區(qū)和兩塊較小的Survivor 空間(你可以叫做From 或者To,也可以叫做 Survivor1 和 Survivor2)

專門研究表明,新生代中的對象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活著的對象一次性地復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。

HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的 90%(80%+10%)只有10%的內(nèi)存會被“浪費”。當然,98% 的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于 10% 的對象存活,當 Survivor 空間不夠用時,需要依賴其他內(nèi)存(這里指老年代)進行分配擔保(Handle Promotion)

老年代

標記-清除算法(Mark-Sweep)

算法分為“標記”和“清除”兩個階段:首先掃描所有對象標記出需要回收的對象,在標記完成后掃描回收所有被標記的對象,所以需要掃描兩遍。回收效率略低,如果大部分對象是朝生夕死,那么回收效率降低,因為需要大量標記對象和回收對象,對比復制回收效率要低。

它的主要問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動作。回收的時候如果需要回收的對象越多,需要做的標記和清除的工作越多,所以標記清除算法適用于老年代。

標記-整理算法(Mark-Compact)

首先標記出所有需要回收的對象,在標記完成后,后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存。標記整理算法雖然沒有內(nèi)存碎片,但是效率偏低。

我們看到標記整理與標記清除算法的區(qū)別主要在于對象的移動。對象移動不單單會加重系統(tǒng)負擔,同時需要全程暫停用戶線程才能進行,同時所有引用對象的地方都需要更新(直接指針需要調(diào)整)。

三色標記

三色標記最大的好處是可以異步執(zhí)行,可以以極少的中斷時間或者沒有中斷來進行整個 GC。

三色標記.png

三色

  • 黑色:根對象,或者該對象與它的子對象都被掃描過
  • 灰色:本身被掃描,但是還沒掃描完該對象的子對象
  • 白色:未被掃描對象,如果掃描完所有對象之后,最終為白色的為不可達對象,既垃圾對象

并發(fā)情況下的漏標問題

  • CMS 解決方法:Incremental Update 算法
    • 當一個白色對象被一個黑色對象引用,將黑色對象重新標記為灰色,讓垃圾回收器重新掃描。
  • G1 解決方法:SATB(snapshot-at-the-beginning)
    • 剛開始做一個快照,當一個灰色對象指向白色的引用消失時要把這個引用推到 GC 的堆棧(GC 方法運行時數(shù)據(jù)也是來自棧中),保證對象還能被 GC 掃描到,下回直接掃描就行了,白色就不會漏標。

對比:G1 在處理并發(fā)標記的過程比 CMS 效率要高

  • SATB 算法是關(guān)注引用的刪除。Incremental Update 算法關(guān)注引用的增加。

  • G1 如果使用 Incremental Update 算法,因為變成灰色的成員還要重新掃,重新再來一遍,效率太低了。

  • 所以G1 在處理并發(fā)標記的過程比CMS 效率要高,這個主要是解決漏標的算法決定的。

常見的垃圾回收器

  • 并行:垃圾收集的多線程的同時進行
  • 并發(fā):垃圾收集的多線程和應(yīng)用的多線程同時進行
  • 吞吐量:運行用戶代碼時間 / ( 運行用戶代碼時間 + 垃圾收集時間 )
  • 垃圾收集時間:垃圾回收頻率 * 單次垃圾回收時間
  • 垃圾回收器的三項指標:延時、吞吐量、內(nèi)存占用
    • 傳統(tǒng)的垃圾回收器一般情況下內(nèi)存占用、吞吐量、延時只能同時滿足兩個。但是現(xiàn)在的發(fā)展,延遲這項的目標越來越重要。所以就有低延遲的垃圾回收器。
垃圾回收器.png
收集器 收集對象和算法 收集器類型 說明 場景
Serial 新生代
復制算法
單線程 簡單高效;適合內(nèi)存不大的情況
ParNew 新生代
復制算法
并行的多線程收集器 是 Serial 的多線程版 與 CMS 搭配
Parallel Scavenge 新生代
復制算法
并行的多線程收集器 類似 ParNew,面向吞吐量 是 Server 級別多 CPU 機器上的默認 GC 方式,主要適合后臺運算且不需要太多交互的任務(wù)
Serial Old 老年代
標記 - 整理算法
單線程 Client 模式下虛擬機使用
Parrallel Old 老年代
標記 - 整理算法
并行的多線程收集器 面向吞吐量 在注重吞吐量以及 CPU 資源敏感的場合使用
CMS 老年代
標記 - 清除算法
并行與并發(fā)收集器 盡可能地縮短 STW,缺點:
1. 至少 4 核 CPU
2. 內(nèi)存碎片,使用 Serial Old 整理碎片
3. 浮動垃圾,需要更大的堆空間,6G 以上
重視服務(wù)的響應(yīng)速度、系統(tǒng)停頓時間和用戶體驗的互聯(lián)網(wǎng)網(wǎng)站或者B/S系統(tǒng)?;ヂ?lián)網(wǎng)后端目前 CMS 是主流的垃圾回收器。
G1 全部
化整為零 Region
標記 - 整理 & 復制
并行與并發(fā)收集器 采用分區(qū)回收的思維,基本不犧牲吞吐量的前提下完成低停頓的內(nèi)存回收;可預測的停頓是其最大的優(yōu)勢 面向服務(wù)端應(yīng)用的垃圾回收器,目標為取代 CMS

Serial/Serial Old(SerialGC)

開啟:-XX:+UseSerialGC ,串行垃圾收集器,幾十兆 ~ 一兩百兆

JVM 剛誕生就只有這種,最古老的,單線程,獨占式,成熟,適合單 CPU,一般用在客戶端模式下使用。這種垃圾回收器只適合幾十兆到一兩百兆的堆空間進行垃圾回收,可以控制停頓時間再100ms 左右,但是對于超過這個大小的內(nèi)存回收速度很慢。

Parallel Scavenge/Parallel Old(ParallerGC)

開啟:-XX:+UseParallelGC,并行垃圾收集器(吞吐量優(yōu)先垃圾回收器),上百兆~幾G

為了提高回收效率,從 JDK1.3 開始,JVM 使用了多線程的垃圾回收機制,關(guān)注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用 CPU 時間,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的任務(wù)。適合回收堆空間上百兆~幾個G。

參數(shù)

  • -XX:MaxGCPauseMillis最大停頓時間
    • 垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間為代價換取的:系統(tǒng)把新生代調(diào)得小一些,收集 300MB 新生代肯定比收集 500MB 快,但這也導致垃圾收集發(fā)生得更頻繁,原來 10 秒收集一次、每次停頓 100 毫秒,現(xiàn)在變成 5 秒收集一次、每次停頓 70 毫秒。停頓時間的確在下降,但吞吐量也降下來了。
  • -XX:GCTimeRatio垃圾收集時間占總時間的比率
    • 參數(shù)的值則應(yīng)當是一個大于 0 小于 100 的整數(shù)。垃圾收集時間占總時間的比率,相當于吞吐量的倒數(shù)。
    • 例如:把此參數(shù)設(shè)置為 19, 那允許的最大垃圾收集時占用總時間的 5% (即1/(1+19))
    • 默認值為 99,即允許最大 1% (即1/(1+99))的垃圾收集時間
  • -XX:+UseAdaptiveSizePolicy動態(tài)調(diào)整(默認開啟)
    • 不需要人工指定新生代的大小(-Xmn)、Eden 與 Survivor 區(qū)的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節(jié)參數(shù)了,虛擬機會根據(jù)當前系統(tǒng)的運行情況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量。

ParNew/Concurrent Mark Sweep(CMSGC)

開啟:-XX:+UseConcMarkSweepGC ,第一個并發(fā)垃圾收集器,幾G ~ 20G

  • ParNew 是多線程垃圾回收器。和 Serial 唯一的區(qū)別是多線程,停頓時間比 Serial 少。

    • 在 JDK9 以后,把 ParNew 合并到了 CMS 了。
  • CMS 是一種以獲取最短回收停頓時間為目標的收集器,重視服務(wù)的響應(yīng)速度。由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作,所以總體上看CMS 的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。

    • 在 JDK1.8 以后,CMS 不能與 Serial 配對使用,只能調(diào)用 Serial Old。在 JDK9 CMS 進入廢棄倒計時,在 JDK14 正式移除 CMS。

流程

  1. 初始標記(STW):短暫,僅僅只是標記一下 GC Roots 能直接關(guān)聯(lián)到的對象,速度很快
  2. 并發(fā)標記:并發(fā)處理;三色標記產(chǎn)生漏標
    • 標記從 GC Roots 開始關(guān)聯(lián)的所有對象,開始遍歷整個可達分析路徑的對象。這個時間比較長,所以采用并發(fā)處理(垃圾回收器線程和用戶線程同時工作)
  3. 重新標記(STW):短暫
    • 為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分對象的標記記錄。就是三色標記產(chǎn)生的漏標,使用 Incremental Update 算法來修正。
    • 這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短。
  4. 并發(fā)清除

缺點

  1. CPU 敏感:至少 4 核
  2. 浮動垃圾
    • 由于CMS 并發(fā)清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標記過程之后,CMS 無法在當次收集中處理掉它們,只好留待下一次 GC 時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
    • 因此需要預留出一部分內(nèi)存,意味著CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。在1.6 的版本中老年代空間使用率閾值(92%)
    • 如果預留的內(nèi)存不夠存放浮動垃圾,就會出現(xiàn) Concurrent Mode Failure,這時虛擬機將臨時啟用 Serial Old 來替代CMS。
  3. 空間碎片:標記 - 清除算法會導致產(chǎn)生不連續(xù)的空間碎片

參數(shù)

  • -XX:+UseCMSCompactAtFullCollection無法分配大對象時使用 Serial Old 進行碎片整理(默認開啟)
    • CMS 會有內(nèi)存碎片,給大對象的分配帶來很大的麻煩
    • 如果分配不了大對象,就進行內(nèi)存碎片的整理過程。這個地方一般會使用 Serial Old ,因為 Serial Old 是一個單線程,所以如果內(nèi)存空間很大、且對象較多時,CMS 發(fā)生這樣的情況會很卡。

為什么 CMS 采用標記 -清除

在實現(xiàn)并發(fā)的垃圾回收時,如果采用標記整理算法,那么還涉及到對象的移動,對象的移動必定涉及到引用的變化,這個需要暫停業(yè)務(wù)線程來處理棧信息,這樣使得并發(fā)收集的暫停時間更長,所以使用簡單的標記-清除算法才可以降低 CMS 的 STW 的時間。

Garbage First(G1)

開啟:-XX:+UseG1GC并發(fā)垃圾收集器,6G ~ 上百G

大于 6G 直接用 G1,不用 CMS,省事,沒那么多煩惱。

設(shè)計思想(采用 Region)

為了實現(xiàn) STW 的時間可預測,G1 將堆內(nèi)存“化整為零”,將堆內(nèi)存劃分成多個大小相等獨立區(qū)域(Region),每一個 Region 都可以根據(jù)需要,扮演新生代的 Eden 空間、Survivor 空間,或者老年代空間。回收器能夠?qū)Π缪莶煌巧?Region 采用不同的策略去處理。

Region

  • 新生代(Eden,Survivor)
  • 老年代
    • Old
    • Humongous
      • 存儲大對象,大小超過了一個 Region 容量一半的對象即可判定為大對象
      • 超過了 Region 容量的大對象,會被存放在 N 個連續(xù)的 Humongous Region 之中

流程

  1. 初始標記(Initial Marking),STW:短暫;TAMS
    • 僅僅只是標記一下 GC Roots 能直接關(guān)聯(lián)到的對象,并且修改 TAMS 指針的值,讓下一階段用戶線程并發(fā)運行時,能正確地在可用的 Region 中分配新對象。
    • 這個階段需要停頓線程,但耗時很短,而且是借用進行 Minor GC 的時候同步完成的,所以 G1 收集器在這個階段實際并沒有額外的停頓。
    • TAMS:要達到 GC 與用戶線程并發(fā)運行,必須要解決回收過程中新對象的分配,所以 G1 為每一個 Region 區(qū)域設(shè)計了兩個名 TAMS (Top at Mark Start)的指針,從 Region 區(qū)域劃出一部分空間用于記錄并發(fā)回收過程中的新對象。這樣的對象認為它們是存活的,不納入垃圾回收范圍。
  2. 并發(fā)標記( Concurrent Marking):三色標記產(chǎn)生漏標
    • 從 GC Root 開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。
    • 當對象圖掃描完成以后, 并發(fā)時有引用變動的對象, 這些對象會漏標 , 漏標的對象會被一個叫做 SATB (snapshot-at-the-beginning) 算法來解決。
  3. 最終標記( Final Marking),STW:短暫;處理 SATB 記錄
    • 處理并發(fā)階段結(jié)后仍遺留下來的最后那少量的 SATB 記錄(漏標對象)。
  4. 篩選回收( Live Data Counting and Evacuation),STW:根據(jù)用戶所期望的停頓時間來制定回收計劃
    • 負責更新 Region 的統(tǒng)計數(shù)據(jù),對各個Region 的回收價值和成本進行排序,根據(jù)用戶所期望的停頓時間來制定回收計劃
    • 可以自由選擇任意多個 Region 構(gòu)成回收集,然后把決定回收的那一部分 Region 的存活對象復制到空的 Region 中,再清理掉整個舊 Region 的全部空間

特點

  • 并行與并發(fā)
  • 分代收集
  • 空間整合
    • 與 CMS 的“標記—清理”算法不同,G1 從整體來看是基于“標記—整理”算法實現(xiàn)的收集器,從局部(兩個Region 之間)上來看是基于“復制”算法實現(xiàn)的,但無論如何,這兩種算法都意味著 G1 運作期間不會產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。
    • 這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次 GC。
  • 追求停頓時間
    • G1 嘗試調(diào)整新生代和老年代的比例,堆大小,晉升年齡來達到這個目標時間。

參數(shù)

  • -XX:G1HeapRegionSize: Region 大小,1MB ~ 32MB ,2 的 N 次冪
    • 一般建議逐漸增大該值,隨著 size 增加,垃圾的存活時間更長,GC 間隔更長,但每次 GC 的時間也會更長。
  • -XX:MaxGCPauseMillis: 最大 GC 暫停時間

低延遲的垃圾回收器

  • Eplison:不能進行垃圾回收
    • 這個垃圾回收器不能進行垃圾回收,是一個“不干活”的垃圾回收器,由 RedHat 退出,它還要負責堆的管理與布局、對象的分配、與解釋器的協(xié)作、與編譯器的協(xié)作、與監(jiān)控子系統(tǒng)協(xié)作等職責,主要用于需要剝離垃圾收集器影響的性能測試和壓力測試
  • ZGC:類似于G1 的 Region;染色指針;STW 小于 10ms
    • 有類似于G1 的Region,但是沒有分代。標志性的設(shè)計是染色指針 ColoredPointers(這個概念了解即可),染色指針有 4TB 的內(nèi)存限制,但是效率極高,它是一種將少量額外的信息存儲在指針上的技術(shù)。
    • 它可以做到幾乎整個收集過程全程可并發(fā),短暫的 STW 也只與 GC Roots 大小相關(guān)而與堆空間內(nèi)存大小無關(guān),因此可以實現(xiàn)任何堆空間 STW 的時間小于十毫秒的目標。
    • JDK 13 支持的內(nèi)存增大到 16TB
    • JDK 14 支持在 Windows 和 MAC 上使用
    • JDK 15 正式上線
  • Shenandoah:類似于G1 的 Region;染色指針;STW 幾十毫秒
    • 第一款非 Oracle 公司開發(fā)的垃圾回收器,有類似于 G1 的 Region,但是沒有分代。也用到了染色指針 ColoredPointers。效率沒有 ZGC 高,大概幾十毫秒的目標。
    • JDK 15 上線

RSet/CardTable

處理跨代引用或 G1 中的跨 Region 引用的問題。

卡表.png

RSet(記憶集)

RSet 本身就是一個 Hash 表。RSet 的價值在于使得垃圾收集器不需要掃描整個堆,找到誰引用了當前分區(qū)中的對象,只需要掃描 RSet 即可。

  • G1:每個 Region 都有,非常消耗空間
    • 記錄了其他 Region 中的對象到本 Region 的引用, 在一個 Region 區(qū)里面。
    • 每一個 Region 都需要一個 RSet 的內(nèi)存區(qū)域,導致有 G1 的 RSet 可能會占據(jù)整個堆容量的 20% 乃至更多。
  • CMS:只有一份

CardTable(卡表)

如果一個老年代 CardTable 中有對象指向新生代, 就將它設(shè)為 Dirty(標志位1), 下次掃描時,只需要掃描 CardTable 上是 Dirty 的內(nèi)存區(qū)域即可。
字節(jié)數(shù)組 CARDTABLE 的每一個元素都對應(yīng)著其標識的內(nèi)存區(qū)域中一塊特定大小的內(nèi)存塊,這個內(nèi)存塊被稱作“卡頁”(Card Page)。一般來說,卡頁大小都是以 2 的 N 次冪的字節(jié)數(shù),假設(shè)使用的卡頁是 2 的 10 次冪,即 1K,內(nèi)存區(qū)域的起始地址是 0x0000 的話,數(shù)組 CARD_TABLE 的第 0、1、2 號元素,分別對應(yīng)了地址范圍為 0x0000~0x03FF、0x0400 ~ 0x07FF 0x0800~0x011FF 的卡頁內(nèi)存。

  • G1:按 Region 劃分,所以要求 Region 大小為 2 的 N 次冪
  • CMS:按 2 的 N 次冪劃分

安全點與安全區(qū)域

安全點

用戶線程暫停,GC 線程要開始工作,但是要確保用戶線程暫停的這行字節(jié)碼指令是不會導致引用關(guān)系的變化。所以 JVM 會在字節(jié)碼指令中,選一些指令,作為“安全點”,比如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,一般是這些指令才會產(chǎn)生安全點。

為什么它叫安全點,是這樣的,GC 時要暫停業(yè)務(wù)線程,并不是搶占式中斷(立馬把業(yè)務(wù)線程中斷)而是主動是中斷。

主動式中斷是設(shè)置中斷標志,各業(yè)務(wù)線程在運行過程中會不停的主動去輪詢這個標志,一旦發(fā)現(xiàn)中斷標志為True,就會在自己最近的“安全點”上主動中斷掛起。

安全區(qū)域

要是業(yè)務(wù)線程都不執(zhí)行(業(yè)務(wù)線程處于Sleep 或者是Blocked 狀態(tài)),那么程序就沒辦法進入安全點,對于這種情況,就必須引入安全區(qū)域。

安全區(qū)域是指能夠確保在某一段代碼片段之中, 引用關(guān)系不會發(fā)生變化,因此,在這個區(qū)域中任意地方開始垃圾收集都是安全的。我們也可以把安全區(qū)城看作被擴展拉伸了的安全點。

安全區(qū)域.png

當用戶線程執(zhí)行到安全區(qū)域里面的代碼時,首先會標識自己已經(jīng)進入了安全區(qū)域,這段時間里 JVM 要發(fā)起 GC 就不必去管這個線程了。 當線程要離開安全區(qū)域時,它要看 JVM 是否已經(jīng)完成了(根節(jié)點枚舉,或者其他 GC 中需要暫停用戶線程的階段)

  1. 如果完成了,那線程就當作沒事發(fā)生過,繼續(xù)執(zhí)行。
  2. 否則它就必須一直等待, 直到收到可以離開安全區(qū)域的信號為止。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容