vivo 校招:說(shuō)一說(shuō) JVM 垃圾回收算法有哪些?分別用在哪些垃圾收
大家好,我是碼哥,可以叫我靚仔。
最近 vivo 校招薪資開(kāi)獎(jiǎng)了,想必互聯(lián)網(wǎng)公司給的不算多,有的同學(xué)達(dá)到 vivo offer 后直接拒了。
vivo 的面試難度如何?
下面就分享一位校招同學(xué)在 vivo Java 后端崗位面試中就有問(wèn)到 JVM 垃圾回收算法以及這些算法分別用在哪些垃圾收集器?
vivo 一面
JVM 垃圾回收時(shí)自動(dòng)管理內(nèi)存的一種機(jī)制,用于釋放不再使用的對(duì)象所占用的內(nèi)存空間。
一般是通過(guò)兩個(gè)步驟實(shí)現(xiàn):
- 標(biāo)記階段:識(shí)別可回收的垃圾對(duì)象。
- 清除階段:回收標(biāo)記為垃圾的對(duì)象所占的內(nèi)存。
下面我們先來(lái)看下垃圾的標(biāo)記是如何實(shí)現(xiàn)的。
垃圾標(biāo)記方式
通常,標(biāo)記垃圾有兩種方式:引用計(jì)數(shù)和可達(dá)性分析。
引用計(jì)數(shù)
通過(guò)維護(hù)每個(gè)對(duì)象的引用計(jì)數(shù)來(lái)判斷對(duì)象是否可以被回收。當(dāng)有一個(gè)指針引用它,那么引用計(jì)數(shù)+1,當(dāng)引用計(jì)數(shù)為 0 時(shí),表示沒(méi)有被對(duì)象引用,可以被回收。
但是引用計(jì)數(shù)會(huì)存在一個(gè)問(wèn)題,就是對(duì)象互相引用會(huì)導(dǎo)致-循環(huán)引用,形成一個(gè)環(huán)狀,這樣在這個(gè)循環(huán)引用的環(huán)內(nèi)所有對(duì)象的引用次數(shù)至少都為 1,那么這些對(duì)象永遠(yuǎn)無(wú)法被回收了。
可達(dá)性分析算法
Java 采用的也是這一種,可達(dá)性分析算法表示從 GC Roots 為起點(diǎn),開(kāi)始查找存活對(duì)象,在查找過(guò)的路徑稱為引用鏈,所有能訪問(wèn)到對(duì)象標(biāo)記為“可達(dá)”,無(wú)法訪問(wèn)到的對(duì)象就是不可達(dá),也就是可以被回收的垃圾。
哪些可以作為 GC ROOTS 對(duì)象呢?
- 虛擬機(jī)棧的引用:方法中的局部變量。
- 方法區(qū)中的類靜態(tài)屬性、常量引用的對(duì)象。
- JNI(Java Native Interface)引用:本地方法持有的對(duì)象引用。
- 正在被線程引用的對(duì)象。
那么在找到垃圾后,如何進(jìn)行回收垃圾呢?
垃圾回收算法
標(biāo)記-清除算法
主要分為“標(biāo)記”和“清除”兩個(gè)階段,標(biāo)記存在引用的對(duì)象,回收未被標(biāo)記的對(duì)象空間。
存在問(wèn)題:
- 效率不高,因?yàn)樾枰獦?biāo)記的對(duì)象太多。
- 存在大量不連續(xù)的空間碎片
復(fù)制算法
主要是將內(nèi)存分為大小相同的兩部分,每次只使用其中一個(gè),當(dāng)其中一個(gè)的內(nèi)存使用完時(shí),把存活的對(duì)象復(fù)制到另一邊去,然后把剩下的空間清理掉。
這樣可以提高一定效率,但缺點(diǎn)是內(nèi)存空間使用不高。
標(biāo)記-整理算法
標(biāo)記過(guò)程和“標(biāo)記-清除”算法一致,但在回收階段,它是讓所有存活的對(duì)象移動(dòng)至一端,然后清理掉邊界以外的對(duì)象。
分代收集算法
分代收集算法主要是將 Java 堆分為年輕代和老年代兩個(gè)區(qū)域。
- 年輕代:年輕代的絕大多數(shù)對(duì)象都是朝生夕亡的,每次回收只需要關(guān)注如何保存少量存活的對(duì)象,而不是標(biāo)記大量即將回收的對(duì)象。
- 老年代:老年代的絕大多數(shù)對(duì)象是存活時(shí)間較長(zhǎng)的對(duì)象。
垃圾收集器
垃圾收集器是 JVM 中對(duì)垃圾回收算法的具體實(shí)現(xiàn)。
Serial 收集器
Serial(串行)收集器是最基本的垃圾收集器了,它是一個(gè)單線程收集器,進(jìn)行垃圾回收時(shí),只會(huì)用一個(gè)線程去完成垃圾回收工作,同時(shí)會(huì)讓其他所有的工作線程停止(Stop The World),等待它執(zhí)行完成。
Parallel Scavengel 收集器
Parallel Scavengel 收集器其實(shí)就是 Serial 的多線程版本,使用多線程進(jìn)行垃圾回收,而它的系統(tǒng)吞吐量自然也是比 Serial 的要高。
ParNew 收集器
ParNew 收集器和 Parallel Scavengel 收集器十分類似,通常會(huì)和 CMS 結(jié)合使用,新生代采用它完成垃圾回收。
CMS 收集器
CMS,Concurrent Mark Sweep,是一款追求以最短回收時(shí)間為目標(biāo)的垃圾收集器,注重提升用戶體驗(yàn),它是第一次實(shí)現(xiàn)了同時(shí)讓垃圾回收線程和用戶線程一起工作。
CMS 是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過(guò)程有以下幾個(gè)步驟:
- 初始標(biāo)記:暫停所有的線程,記錄下 GC Roots 直接引用到的對(duì)象,這個(gè)過(guò)程耗時(shí)非常短。
- 并發(fā)標(biāo)記:這個(gè)過(guò)程是和用戶線程并發(fā)運(yùn)行的,就是從 GC Roots 直接引用對(duì)象開(kāi)始遍歷,雖然耗時(shí)較長(zhǎng),但也不影響用戶程序,但是會(huì)存在一個(gè)問(wèn)題就是,因?yàn)橛脩艟€程是不暫停的(Stop The World),可能有些已經(jīng)標(biāo)記過(guò)的對(duì)象狀態(tài)會(huì)發(fā)生改變。
- 重新標(biāo)記:這個(gè)過(guò)程就是為了修正上一階段因?yàn)橛脩艟€程導(dǎo)致的已經(jīng)標(biāo)記過(guò)對(duì)象的狀態(tài)發(fā)生改變的記錄,主要處理多標(biāo)、漏標(biāo)問(wèn)題。這個(gè)過(guò)程會(huì)比初始標(biāo)記階段的耗時(shí)長(zhǎng),但也遠(yuǎn)低于并發(fā)標(biāo)記階段。
- 并發(fā)清理:和用戶線程并發(fā)運(yùn)行,GC 線程對(duì)為標(biāo)記的區(qū)域清理。
- 并發(fā)重置:重置本次 GC 的標(biāo)記數(shù)據(jù)。
從上面過(guò)程就可看出,CMS 的主要特點(diǎn)是:并發(fā)收集,延遲低。但也存在幾個(gè)缺點(diǎn):
- 占用 CPU 資源
- 無(wú)法處理浮動(dòng)垃圾,即并發(fā)清理階段中產(chǎn)生新的垃圾,只能等到下一次 GC 再清理。
- 因?yàn)樗褂玫氖恰皹?biāo)記-清除”算法,這個(gè)會(huì)產(chǎn)生大量?jī)?nèi)存空間碎片。
- 某些情況下,會(huì)導(dǎo)致 CMS 退化成 Serial Old 垃圾收集器,比如上一次老年代存在大量垃圾未收集完成,這時(shí)垃圾回收又被觸發(fā)。
CMS 有哪些常用的參數(shù)呢?
-XX:+UseConcMarkSweepGC,啟用 CMS 收集器,注意 JDK8 默認(rèn)使用的是 Parallel GC,JDK9 以后使用 G1 GC。
-XX:ConcGCThreads,CMS 并發(fā)過(guò)程運(yùn)行的線程數(shù)。
-XX:+UseCMSCompactAtFullCollection,F(xiàn)ullGC 完成后再做壓縮整理,針對(duì) CMS 容易產(chǎn)生內(nèi)存碎片做的優(yōu)化。
-XX:CMSFullGCsBeforeCompaction,配合上面使用,多少次 FullGC 完成后進(jìn)行壓縮,,默認(rèn)是 0,即每次都會(huì)壓縮。
-XX:CMSInitiatingOccupancyFraction,老年代使用達(dá)到的某個(gè)比例時(shí)會(huì)觸發(fā) FullGC,默認(rèn)是 92。
-XX:+CMSParallellnitialMarkEnabled,表示在初始標(biāo)記階段采用多線程執(zhí)行,減少 STW 時(shí)間。
-XX:+CMSParallelRemarkEnabled,表示在重新標(biāo)記階段采用多線程執(zhí)行,減少 STW 時(shí)間。
G1 收集器
G1 是一面向服務(wù)器的垃圾收集器,主要針對(duì)多處理器和大內(nèi)存的機(jī)器,在極高概率滿足 GC 停頓時(shí)間要求的同時(shí),具備高吞吐量特性。
G1 將 Java 堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),JVM 最多可以有 2048 個(gè) Region。一般 Region 大小等于堆大小除以 2048,比如堆大小為 4096M,則 Region 大小為 2M。
G1 保留了年輕代和老年代的概念,但不再是物理隔閡了,它們都是(可以不連續(xù))Region 的集合。默認(rèn)年輕代對(duì)堆內(nèi)存的占比是 5%,如果堆大小為 4096M,那么年輕代占據(jù) 200MB 左右的內(nèi)存,對(duì)應(yīng)大概是 100 個(gè) Region。
G1 回收步驟:
- 初始標(biāo)記:暫停所有的線程,記錄下 GC Roots 直接引用到的對(duì)象,這個(gè)過(guò)程耗時(shí)非常短。
- 并發(fā)標(biāo)記:同 CMS。
- 最終標(biāo)記:和 CMS 的重新標(biāo)記一樣。
- 篩選回收:首先對(duì)各 Region 的回收價(jià)值和成本進(jìn)行計(jì)算,根據(jù)用戶設(shè)定的 GC 停頓時(shí)間(-XX:MaxGCPauseMillis 參數(shù))來(lái)制定回收計(jì)劃,比如此時(shí)又 1000 個(gè) Region 需要回收,但是用戶設(shè)置的停頓時(shí)間是 200ms,那么通過(guò)之前回收成本計(jì)算,只會(huì)回收其中部分 Region 比如 600 個(gè),所以時(shí)間是用戶可控的?;厥账惴ㄖ饕玫膹?fù)制算法,把一個(gè) Region 存活的對(duì)象復(fù)制到另外一個(gè) Region 中,所以不會(huì)像 CMS 那樣存在內(nèi)存碎片。