深入學(xué)習(xí)JVM:(4) 垃圾收集算法與垃圾收集器

一. 前言

今天總結(jié)與分享的是垃圾收集算法與垃圾收集器. 有了前幾篇的文章的鋪墊, 我們知道, 這些知識是Jvm調(diào)優(yōu)的前提, 也是面試時高頻提問的重點. 其實說白了, Jvm調(diào)優(yōu)就是盡量減少Full gc, 因為它非常耗時. 而觸發(fā)Full gc的前提大多是堆內(nèi)存中的老年代滿了, 所以我們需要通過選擇垃圾收集器, 調(diào)整Jvm內(nèi)存分代區(qū)域大小等方法讓大多數(shù)對象不進(jìn)入老年代, 使之在Minor gc時就被回收掉.

二. 垃圾收集算法

Jvm中的垃圾收集算法總共有四種: 復(fù)制算法、標(biāo)記清除算法、標(biāo)記整理算法、分代收集算法

1. 復(fù)制算法

個人習(xí)慣, 先上圖:

垃圾收集 復(fù)制算法.png

可以看到, 復(fù)制算法就是把一整塊內(nèi)存, 平分成兩半. 使用時, 只使用其中之一, 垃圾收集時, 直接將有用的對象復(fù)制到另一半內(nèi)存中, 然后清空掉之前使用的這一半內(nèi)存.

優(yōu)點: 效率十分之高
缺點: 內(nèi)存空間使用率低, 能使用的內(nèi)存只有一半, 1個G就剩512M了.

2. 標(biāo)記清除算法

圖:

垃圾收集 標(biāo)記清除算法.png

我們的標(biāo)記清除算法很直觀, 就是將無用對象標(biāo)記一下, 然后清除.

優(yōu)點: 簡單
缺點:
① 空間使用率低(幾次垃圾收集后, 會產(chǎn)生大量不連續(xù)的內(nèi)存碎片)
② 效率問題(如果內(nèi)存較大, 需要收集的對象較多, 那么標(biāo)記與收集將會比較耗時)

3. 標(biāo)記整理算法

圖:

垃圾收集 標(biāo)記整理算法.png

其實標(biāo)記整理算法和標(biāo)記清除算法差不多, 只不過它是標(biāo)記出有用的對象, 然后將這些對象向內(nèi)存的一端移動. 說不上優(yōu)缺點, 但肯定, 效率是沒有復(fù)制算法高的.

4. 分代收集算法

這其實不是一種算法, 就是之前說的將內(nèi)存分為不同的區(qū)域, 再針對這些區(qū)域采用不同的垃圾收集算法. 分區(qū)域是指年輕代和老年代, 年輕代又分為3塊兒, eden區(qū)和2個survivor區(qū). 一般情況下, 年輕代會采用復(fù)制算法, 因為有多個區(qū)域, 復(fù)制算法效率上很占優(yōu)勢. 又因為2個survivor區(qū)總共只占用了年輕代的2/10(默認(rèn)情況下), 所以空間上也沒有很大的問題. 老年代一般就是采用標(biāo)記整理或標(biāo)記清除算法.

三. 垃圾收集器

常見及常用的垃圾收集器總共有如下五種:
Serial收集器、Parallel收集器、ParNew收集器、CMS收集器G1收集器
其中CMS收集器和G1收集器是面試提問的重點, 下面我就來一一描述一下這些垃圾收集器的特點吧.

1. Serial垃圾收集器

圖:

垃圾收集 Serial垃圾收集.png

Serial垃圾收集器收集垃圾, 會STW(stop the world), 停止掉用戶的所有工作線程, 然后開啟一個線程收集垃圾. 收集完畢后, 再恢復(fù)用戶線程的運行.

Serial垃圾收集器可以用在年輕代, 也可以用在老年代. 這是很多年前才會使用的垃圾收集器, 因為它停掉用戶線程, 然后卻只開了一個線程收集垃圾, 效率可想而知.

如果要使用該垃圾收集器, 可以配置如下參數(shù):
年輕代算法: 復(fù)制算法; 年輕代參數(shù): -XX:+UseSerialGC
老年代算法: 標(biāo)記整理算法; 老年代參數(shù): -XX:+UseSerialOldGC

2. Parallel垃圾收集器

圖:

垃圾收集 Parallel垃圾收集.png

為了優(yōu)化Serial垃圾收集器的性能, 出現(xiàn)了Parallel垃圾收集器. 但其實并沒有提供很高深的優(yōu)化, 可以看出, 和Serial垃圾收集器過程幾乎是一樣的, 只不過是在垃圾收集時多加了幾個線程.

可以用在年輕代, 也能用在老年代

如果要使用該垃圾收集器, 可以配置如下參數(shù):
年輕代算法: 復(fù)制算法; 年輕代參數(shù): -XX:+UseParallelGC
老年代算法: 標(biāo)記整理算法; 老年代參數(shù): -XX:+UseParallelOldGC

3. ParNew垃圾收集器

圖:

垃圾收集 ParNew垃圾收集.png

可能會有小伙伴兒懵圈兒了...哎? 你這圖和Parallel收集器不是一樣的嗎? 哈哈哈, 確實. ParNew收集器和Parallel收集器的過程是一樣的. 區(qū)別在于: ParNew只能用在年輕代, 且除了Serial收集器, 只有它能和CMS垃圾收集器配合使用.

如果要使用該垃圾收集器, 可以配置如下參數(shù):
年輕代算法: 復(fù)制算法; 年輕代參數(shù): -XX:+UseParNewGC

4. CMS垃圾收集器(重點)

CMS垃圾收集器, 全稱Concurrent Mark Sweep. 從名字就可以看出它不簡單, 并發(fā)標(biāo)記清除. 它是第一款真正意義上并發(fā)的垃圾收集器, 并不是說它就不stop the world了.而是說它可以一邊收集垃圾, 一邊運行應(yīng)用程序. 還是先看圖再解釋吧:

垃圾收集 CMS垃圾收集.png

這個可就比前面說的垃圾收集器復(fù)雜多了, 而且整個垃圾收集過程分為了五個階段:
初始標(biāo)記、并發(fā)標(biāo)記、重新標(biāo)記、并發(fā)清理、并發(fā)重置 下面詳細(xì)說說這幾個階段的事.

1. 初始標(biāo)記: 停止應(yīng)用程序線程, 開啟一個回收線程標(biāo)記非垃圾對象. 使用可達(dá)性分析算法(前幾篇文章講過), 但只會標(biāo)記GC Roots, 所以速度非???
2. 并發(fā)標(biāo)記: 恢復(fù)工作線程, 與回收線程并發(fā)執(zhí)行. 接著上一階段的GC Roots繼續(xù)向下標(biāo)記, 這個過程的耗時非常長, 幾乎可以占到整個垃圾收集過程的80%. 由于沒有停止工作線程, 所以可能會發(fā)生標(biāo)記的非垃圾對象在下一時刻又變成垃圾的問題.
3. 重新標(biāo)記: 又停機工作線程. 因為并發(fā)標(biāo)記階段可能產(chǎn)生一些標(biāo)記錯誤, 所以這一階段主要是修復(fù)這些錯誤的標(biāo)記. 如: 上一階段標(biāo)記的非垃圾對象, 這一時刻已經(jīng)失去引用, 又淪為了垃圾對象.
4. 并發(fā)清理: 恢復(fù)工作線程, 啟動回收線程, 清理掉未被標(biāo)記的垃圾對象. 可能有同學(xué)會疑惑, 工作線程恢復(fù)了, 那不是又會有新的對象產(chǎn)生, 那這新產(chǎn)生的對象也沒被標(biāo)記, 會不會被回收掉呢? 答案是肯定不會, 不然CMS收集器沒人敢用了. 在這個階段新產(chǎn)生的對象會被直接標(biāo)記. 具體下面的三色標(biāo)記算法會講到.
5. 并發(fā)重置: 這一階段很簡單, 就是將之前的標(biāo)記清除掉.

從以上階段可以看處, 這個CMS收集器, 把整個垃圾收集過程打散了. 這樣就使得用戶的感知沒那么明顯. 把這個時間放大個幾十上百倍來理解就是...本來是一次性等10秒, 現(xiàn)在卻變成了總體十分鐘, 每分鐘等1秒. 當(dāng)然..垃圾收集的過程是很快的, 可能整個才耗時1秒左右, 這里不過是放大了收集時間, 便于理解.

關(guān)于CMS收集器必須要注意的是, 它可能會并發(fā)失敗. 什么是并發(fā)失敗? 因為垃圾收集和應(yīng)用程序是并發(fā)執(zhí)行的. 就有可能發(fā)生垃圾收集時, 應(yīng)用程序又在不斷的new對象, 導(dǎo)致老年代放滿, 又觸發(fā)了full gc. 這時, CMS不會再走前面說的階段, 而是采用本文介紹的第一個垃圾收集器: Serial收集器來收集垃圾.
所以為避免這種情況的發(fā)生, 可以使用參數(shù)來調(diào)整老年代觸發(fā)full gc的內(nèi)存占用比例, 比如調(diào)成80%就觸發(fā)full gc, 這樣在并發(fā)清理時, 應(yīng)用程序新new的對象把剩下的內(nèi)存占滿的概率就會大大降低了.

CMS收集器只能用在老年代, 采用的是標(biāo)記清除算法(會產(chǎn)生內(nèi)存碎片, 可通過參數(shù)設(shè)置整理內(nèi)存碎片)
優(yōu)點: 分散垃圾收集過程, 用戶體驗好.
缺點: 收集線程會搶占cpu資源; 無法處理浮動垃圾(并發(fā)收集時產(chǎn)生的垃圾); 采用標(biāo)記清除算法, 會產(chǎn)生內(nèi)存碎片(不連續(xù)的內(nèi)存空間)

CMS核心參數(shù):

  • -XX:+UseConcMarkSweepGC:啟用cms
  • -XX:ConcGCThreads:并發(fā)的GC線程數(shù)
  • -XX:+UseCMSCompactAtFullCollection:FullGC之后進(jìn)行壓縮整理
  • -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后壓縮一次, 默認(rèn)是0, 代表每次FullGC后都會壓縮一次
  • -XX:CMSInitiatingOccupancyFraction: 當(dāng)老年代使用達(dá)到該比例時會觸發(fā)FullGC(默認(rèn)是92, 這是百分比)
  • -XX:+UseCMSInitiatingOccupancyOnly:只使用設(shè)定的回收閾值(上一參數(shù)設(shè)定的值), 如果不指定, Jvm僅在第一次使用設(shè)定值,后續(xù)又會自動調(diào)整.
  • -XX:+CMSScavengeBeforeRemark:在CMS GC前先進(jìn)行一次minor gc,目的在于減少老年代對年輕代的引用,降低CMS GC的標(biāo)記階段時的開銷,一般CMS的GC耗時 80%都在標(biāo)記階段.
  • -XX:+CMSParallellnitialMarkEnabled:表示在初始標(biāo)記的時候多線程執(zhí)行
  • -XX:+CMSParallelRemarkEnabled:在重新標(biāo)記的時候多線程執(zhí)行
  • 更多參數(shù)請自行百度 ^^

5. G1垃圾收集器

想了想, 還是放在下一篇文章寫吧. 下面先說說收集器的標(biāo)記算法.

四. 垃圾收集器底層算法-三色標(biāo)記算法

我們之前說過可達(dá)性分析算法標(biāo)記非垃圾對象的過程, 這個可達(dá)性分析算法已經(jīng)詳細(xì)講解過了, 就是根據(jù)gc roots向下查找并標(biāo)記非垃圾對象. 那這個標(biāo)記具體是怎么做的呢? 其實底層就是接下來要將的三色標(biāo)記算法了, 老規(guī)矩, 先上圖:

三色標(biāo)記.png

三色標(biāo)記算法根據(jù)對象的掃描完整度, 分為了黑灰白三種顏色:

  1. 黑色: 已經(jīng)將一個對象的全部成員變量掃描完畢.
  2. 灰色: 已經(jīng)掃描過該對象, 但是還未將對象的全部變量掃描完畢.
  3. 白色: 還未掃描過該對象.

以上圖說明:

  1. 對象A包含了對象B和對象C兩個成員變量, 并將B和C都掃描到了, 所以將A對象標(biāo)記為黑色.
  2. 對象B沒有成員變量, 直接標(biāo)記為黑色
  3. 對象C包含對象D和對象E兩個成員變量, 但只掃描了D對象, 并未來得及掃描E對象, 所以此時C對象的顏色被標(biāo)記為灰色.
  4. 對象E還未來得及掃描, 所以是初始顏色: 白色(后續(xù)會掃描到)
  5. 所以垃圾回收主要收集的顏色就是白色, 也就是可達(dá)性分析算法掃描結(jié)束后, 并未被標(biāo)記的對象.

看上去好像設(shè)計的相當(dāng)好, 可也存在一些問題:

  • 前面我們說過CMS收集器是一款..收集線程和應(yīng)用程序線程并發(fā)執(zhí)行的垃圾收集器, 那么它就不可避免的會產(chǎn)生一些"浮動垃圾". 所謂浮動垃圾就是在可達(dá)性分析算法標(biāo)記非垃圾對象后, 因為應(yīng)用程序線程的同步運行, 這部分非垃圾對象失去引用又變成了垃圾對象的一種現(xiàn)象. 對于CMS, 它是無法完全清理這部分對象的. 這種現(xiàn)象也被稱之為"多標(biāo)", 就是多標(biāo)記了.

  • 其實還有一種少標(biāo)記了的情況, 也稱為"漏標(biāo)". 多標(biāo)還好, 只是有一部分垃圾在這一次垃圾回收時無法清理掉, 在下一次垃圾回收時就沒問題了. 可是對于漏標(biāo), 那問題就嚴(yán)重了, 因為我們的可達(dá)性分析算法是對標(biāo)記的對象不進(jìn)行被回收. 那么, 漏標(biāo)的非垃圾對象如果被回收了...可想而知, 我們的程序處處都是空指針異常.

  • 那漏標(biāo)是怎么產(chǎn)生的呢? 假設(shè)我們有A, B兩種類型的對象...還是看圖吧...

三色標(biāo)記 漏標(biāo).png

由上圖可以一句話總結(jié): 漏標(biāo)就是因為應(yīng)用程序線程和收集線程的并行, 導(dǎo)致已標(biāo)記為黑色對象的引用指向了未標(biāo)記完成的對象上.

多標(biāo)和漏標(biāo)如何解決?

針對多標(biāo)和漏標(biāo), Jvm提供了兩種解決方式:
一叫做增量更新, 二叫做原始快照.

增量更新: 在并發(fā)收集過程中, 將每一次引用變更的對象重新標(biāo)記為灰色, 這樣一來, 在并發(fā)標(biāo)記階段結(jié)束后, 在重新標(biāo)記階段開始時, 會將這些對象重新掃描并標(biāo)記. 由于重新標(biāo)記是會停止用戶線程的, 所以在重新標(biāo)記結(jié)束時, 內(nèi)存就不存在任何一個為白色的非垃圾對象.
注意: 新new的對象會被直接標(biāo)記為黑色.

原始快照: 在并發(fā)收集過程中, 將每一次引用變更的白色對象都標(biāo)記成黑色. 這樣就徹底避免了漏標(biāo)的情況, 不過可能會產(chǎn)生大量的浮動垃圾.

增量更新和原始快照底層是怎么做的呢?

其實Jvm底層運用了讀寫屏障, 此處的讀寫屏障并不是內(nèi)存中的讀寫屏障. 而是類似于AOP一樣的思想, 再引用的變更前后, 對該引用做了一些手腳. 如將其以另外的形式保存起來.

增量更新: 在引用變更前, 利用寫屏障將引用保存起來. 在重新標(biāo)記階段重新掃描引用對象所在的根節(jié)點.

原始快照: 在引用變更前, 利用寫屏障將引用保存起來. 在重新標(biāo)記階段直接將保存的引用對象標(biāo)記為黑色.

不同的垃圾收集器, 底層采用了不同的實現(xiàn):

  • CMS采用的是增量更新
  • 下篇文章將要講的G1采用的是原始快照
  • 至于為什么? 可能是因為G1的內(nèi)存分散, 對象處于不同的region, 如果采用增量更新, 掃描起來付出的性能較大. 而CMS的老年代處于一塊獨立的內(nèi)存, 使用增量更新雖然也會重新掃描變更引用的對象, 但相較于G1性能開銷會節(jié)省很多, 且能處理掉更多的浮動垃圾.

ok, 今天的知識總結(jié)就到這里. 如果有寫的不對的地方, 仍然希望小伙伴兒們能不吝賜教~

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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