vivo 校招:說一說 JVM 垃圾回收算法有哪些?分別用在哪些垃圾收

大家好,我是碼哥,可以叫我靚仔。
最近 vivo 校招薪資開獎了,想必互聯(lián)網(wǎng)公司給的不算多,有的同學(xué)達(dá)到 vivo offer 后直接拒了。
vivo 的面試難度如何?
下面就分享一位校招同學(xué)在 vivo Java 后端崗位面試中就有問到 JVM 垃圾回收算法以及這些算法分別用在哪些垃圾收集器?

vivo 一面
JVM 垃圾回收時自動管理內(nèi)存的一種機(jī)制,用于釋放不再使用的對象所占用的內(nèi)存空間。
一般是通過兩個步驟實(shí)現(xiàn):
- 標(biāo)記階段:識別可回收的垃圾對象。
- 清除階段:回收標(biāo)記為垃圾的對象所占的內(nèi)存。
下面我們先來看下垃圾的標(biāo)記是如何實(shí)現(xiàn)的。
垃圾標(biāo)記方式
通常,標(biāo)記垃圾有兩種方式:引用計(jì)數(shù)和可達(dá)性分析。
引用計(jì)數(shù)
通過維護(hù)每個對象的引用計(jì)數(shù)來判斷對象是否可以被回收。當(dāng)有一個指針引用它,那么引用計(jì)數(shù)+1,當(dāng)引用計(jì)數(shù)為 0 時,表示沒有被對象引用,可以被回收。

但是引用計(jì)數(shù)會存在一個問題,就是對象互相引用會導(dǎo)致-循環(huán)引用,形成一個環(huán)狀,這樣在這個循環(huán)引用的環(huán)內(nèi)所有對象的引用次數(shù)至少都為 1,那么這些對象永遠(yuǎn)無法被回收了。
可達(dá)性分析算法
Java 采用的也是這一種,可達(dá)性分析算法表示從 GC Roots 為起點(diǎn),開始查找存活對象,在查找過的路徑稱為引用鏈,所有能訪問到對象標(biāo)記為“可達(dá)”,無法訪問到的對象就是不可達(dá),也就是可以被回收的垃圾。

哪些可以作為 GC ROOTS 對象呢?
- 虛擬機(jī)棧的引用:方法中的局部變量。
- 方法區(qū)中的類靜態(tài)屬性、常量引用的對象。
- JNI(Java Native Interface)引用:本地方法持有的對象引用。
- 正在被線程引用的對象。
那么在找到垃圾后,如何進(jìn)行回收垃圾呢?
垃圾回收算法
標(biāo)記-清除算法
主要分為“標(biāo)記”和“清除”兩個階段,標(biāo)記存在引用的對象,回收未被標(biāo)記的對象空間。

存在問題:
- 效率不高,因?yàn)樾枰獦?biāo)記的對象太多。
- 存在大量不連續(xù)的空間碎片
復(fù)制算法
主要是將內(nèi)存分為大小相同的兩部分,每次只使用其中一個,當(dāng)其中一個的內(nèi)存使用完時,把存活的對象復(fù)制到另一邊去,然后把剩下的空間清理掉。
這樣可以提高一定效率,但缺點(diǎn)是內(nèi)存空間使用不高。

標(biāo)記-整理算法
標(biāo)記過程和“標(biāo)記-清除”算法一致,但在回收階段,它是讓所有存活的對象移動至一端,然后清理掉邊界以外的對象。

分代收集算法
分代收集算法主要是將 Java 堆分為年輕代和老年代兩個區(qū)域。
- 年輕代:年輕代的絕大多數(shù)對象都是朝生夕亡的,每次回收只需要關(guān)注如何保存少量存活的對象,而不是標(biāo)記大量即將回收的對象。
- 老年代:老年代的絕大多數(shù)對象是存活時間較長的對象。

垃圾收集器
垃圾收集器是 JVM 中對垃圾回收算法的具體實(shí)現(xiàn)。
Serial 收集器
Serial(串行)收集器是最基本的垃圾收集器了,它是一個單線程收集器,進(jìn)行垃圾回收時,只會用一個線程去完成垃圾回收工作,同時會讓其他所有的工作線程停止(Stop The World),等待它執(zhí)行完成。

Parallel Scavengel 收集器
Parallel Scavengel 收集器其實(shí)就是 Serial 的多線程版本,使用多線程進(jìn)行垃圾回收,而它的系統(tǒng)吞吐量自然也是比 Serial 的要高。

ParNew 收集器
ParNew 收集器和 Parallel Scavengel 收集器十分類似,通常會和 CMS 結(jié)合使用,新生代采用它完成垃圾回收。

CMS 收集器
CMS,Concurrent Mark Sweep,是一款追求以最短回收時間為目標(biāo)的垃圾收集器,注重提升用戶體驗(yàn),它是第一次實(shí)現(xiàn)了同時讓垃圾回收線程和用戶線程一起工作。

CMS 是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程有以下幾個步驟:
- 初始標(biāo)記:暫停所有的線程,記錄下 GC Roots 直接引用到的對象,這個過程耗時非常短。
- 并發(fā)標(biāo)記:這個過程是和用戶線程并發(fā)運(yùn)行的,就是從 GC Roots 直接引用對象開始遍歷,雖然耗時較長,但也不影響用戶程序,但是會存在一個問題就是,因?yàn)橛脩艟€程是不暫停的(Stop The World),可能有些已經(jīng)標(biāo)記過的對象狀態(tài)會發(fā)生改變。
- 重新標(biāo)記:這個過程就是為了修正上一階段因?yàn)橛脩艟€程導(dǎo)致的已經(jīng)標(biāo)記過對象的狀態(tài)發(fā)生改變的記錄,主要處理多標(biāo)、漏標(biāo)問題。這個過程會比初始標(biāo)記階段的耗時長,但也遠(yuǎn)低于并發(fā)標(biāo)記階段。
- 并發(fā)清理:和用戶線程并發(fā)運(yùn)行,GC 線程對為標(biāo)記的區(qū)域清理。
- 并發(fā)重置:重置本次 GC 的標(biāo)記數(shù)據(jù)。
從上面過程就可看出,CMS 的主要特點(diǎn)是:并發(fā)收集,延遲低。但也存在幾個缺點(diǎn):
- 占用 CPU 資源
- 無法處理浮動垃圾,即并發(fā)清理階段中產(chǎn)生新的垃圾,只能等到下一次 GC 再清理。
- 因?yàn)樗褂玫氖恰皹?biāo)記-清除”算法,這個會產(chǎn)生大量內(nèi)存空間碎片。
- 某些情況下,會導(dǎo)致 CMS 退化成 Serial Old 垃圾收集器,比如上一次老年代存在大量垃圾未收集完成,這時垃圾回收又被觸發(fā)。
CMS 有哪些常用的參數(shù)呢?
-XX:+UseConcMarkSweepGC,啟用 CMS 收集器,注意 JDK8 默認(rèn)使用的是 Parallel GC,JDK9 以后使用 G1 GC。
-XX:ConcGCThreads,CMS 并發(fā)過程運(yùn)行的線程數(shù)。
-XX:+UseCMSCompactAtFullCollection,F(xiàn)ullGC 完成后再做壓縮整理,針對 CMS 容易產(chǎn)生內(nèi)存碎片做的優(yōu)化。
-XX:CMSFullGCsBeforeCompaction,配合上面使用,多少次 FullGC 完成后進(jìn)行壓縮,,默認(rèn)是 0,即每次都會壓縮。
-XX:CMSInitiatingOccupancyFraction,老年代使用達(dá)到的某個比例時會觸發(fā) FullGC,默認(rèn)是 92。
-XX:+CMSParallellnitialMarkEnabled,表示在初始標(biāo)記階段采用多線程執(zhí)行,減少 STW 時間。
-XX:+CMSParallelRemarkEnabled,表示在重新標(biāo)記階段采用多線程執(zhí)行,減少 STW 時間。
G1 收集器
G1 是一面向服務(wù)器的垃圾收集器,主要針對多處理器和大內(nèi)存的機(jī)器,在極高概率滿足 GC 停頓時間要求的同時,具備高吞吐量特性。

G1 將 Java 堆劃分為多個大小相等的獨(dú)立區(qū)域(Region),JVM 最多可以有 2048 個 Region。一般 Region 大小等于堆大小除以 2048,比如堆大小為 4096M,則 Region 大小為 2M。
G1 保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續(xù))Region 的集合。默認(rèn)年輕代對堆內(nèi)存的占比是 5%,如果堆大小為 4096M,那么年輕代占據(jù) 200MB 左右的內(nèi)存,對應(yīng)大概是 100 個 Region。
G1 回收步驟:

- 初始標(biāo)記:暫停所有的線程,記錄下 GC Roots 直接引用到的對象,這個過程耗時非常短。
- 并發(fā)標(biāo)記:同 CMS。
- 最終標(biāo)記:和 CMS 的重新標(biāo)記一樣。
- 篩選回收:首先對各 Region 的回收價值和成本進(jìn)行計(jì)算,根據(jù)用戶設(shè)定的 GC 停頓時間(-XX:MaxGCPauseMillis 參數(shù))來制定回收計(jì)劃,比如此時又 1000 個 Region 需要回收,但是用戶設(shè)置的停頓時間是 200ms,那么通過之前回收成本計(jì)算,只會回收其中部分 Region 比如 600 個,所以時間是用戶可控的。回收算法主要用的復(fù)制算法,把一個 Region 存活的對象復(fù)制到另外一個 Region 中,所以不會像 CMS 那樣存在內(nèi)存碎片。



























