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