【後端面經-Java】JVM垃圾回收機制

2023-07-19 21:00:53

1. Where:回收哪裡的東西?——JVM記憶體分配

JVM垃圾回收機制(Garbage Collect,簡稱GC)主要負責回收JVM記憶體當中未被及時釋放回收的記憶體區域,JVM垃圾回收機制讓程式設計師擺脫了手動釋放記憶體的操作,降低了程式設計師疏忽大意導致的風險。
那麼,垃圾回收機制到底針對哪一塊的記憶體空間進行處理呢?是整體記憶體還是某一塊記憶體?
在回答這個問題之前,我們需要先了解一下JVM記憶體分配機制,JVM記憶體分配機制主要有如下幾個區域:

  • 棧(Stack)
  • 堆(Heap)
  • 方法區(Method Area)
  • 本地方法棧(Native Method Stack)
  • 程式計數器(PC)
    我們需要知道的是,棧、本地方法棧、程式計數器和方法呼叫有關,是執行緒私有的,隨著方法結束,棧和本地方法棧的棧幀會出棧,釋放記憶體,因此,這三部分並不需要使用垃圾回收機制。
    而堆主要存放物件範例和陣列,而方法區存放類的資訊、編譯資訊、常數池等,這兩塊區域由於是執行緒共用的,在方法呼叫結束之後也不會被釋放記憶體(畢竟A用完了,不知道B、C、D會不會用到這部分)需要使用垃圾回收機制進行記憶體回收。

關於每個區域的具體內容,可參考部落格:【後端面經-Java】JVM記憶體分割區詳解

因此,記憶體回收機制主要針對堆和方法區進行處理。

2. Which:記憶體物件中誰會被回收?——GC分代思想

2.1 年輕代/老年代/永久代

JVM垃圾回收機制中,一般都是基於GC分代思想進行演演算法設計。
GC機制將記憶體內容分為三部分:

  • 年輕代(Young Generation):新產生的範例基本上都處於這代,因為新產生的範例大部分都是一次性的,因此這部分記憶體需要經常進行記憶體回收;
  • 老年代(Old Generation):在新生代殘酷頻繁的篩選機制中,多次存活下來的範例會進入老年代,老年代意味著生命週期較長,一般在記憶體沒有滿之前不會對這部分記憶體進行回收;
  • 永久代(Permanent Generation,JCK1.8之後改成元空間(Metaspace)):永久代存放的是JVM程式執行相關的後設資料,比如類資訊、方法資訊、常數池等,內容重要且佔空間小,因此基本上不會進行垃圾回收。

如果要類比的話,年輕代就是剛剛步入職場的小青年,不穩定性較高,很容易被裁員(垃圾回收),而熬過這個階段,成為技術骨幹(老年代)之後,基本上不會在正常公司執行過程中被裁員,除非公司倒閉(記憶體已滿),而永久代或者元空間就是公司最高層的管理人員,對公司的執行起著關鍵作用,一般情況下不會被裁員。

2.2 記憶體細分

  1. 堆和方法區
    堆中存放年輕代和老年代,而方法區中存放永久代。
  2. 新生代區域細分
    新生代區域又分為Eden區Survivor0區Survivor1區,比例為8:1:1

整體記憶體細分情況如圖所示:

3. When:什麼時候回收垃圾?——GC觸發條件

GC按照觸發條件,可分為Scavenge GCFull GC

  • Scavenge GC(Minor GC)
    Scavenge GC是指年輕代Eden區的垃圾回收,觸發條件為:
    • 年輕代Eden區域記憶體不足
      • 呼叫Scavenge GC之後,將未清理的元素放入Survivor0區域。如果Survivor0區域記憶體不足,則將Survivor0區域記憶體中的所有元素放入Survivor1區域。清空Survivor0區域,然後將Survivor1中的元素放入Survivor0中;
      • 如果Survivor1區域記憶體不足,則將Survivor1區域記憶體中的所有元素放入老年代中;
      • 如果老年代也記憶體不足,則會考慮觸發Full GC。
  • Full GC(Major GC)
    Full GC是整體對記憶體的垃圾回收,包括年輕代和老年代,觸發條件為:
    • 老年代記憶體不足
    • 永久代記憶體不足
    • 顯性呼叫system.gc()
    • 上次GC之後堆記憶體的劃分出現變化

對於GC執行緒,其本身的優先順序比較低,因此在CPU空閒的時候,可能會進行GC處理,而在忙時基本上不會進行GC處理,除非此時記憶體空間不足,需要GC處理之後才能正常執行。
Full GC對於計算資源是一個很大的消耗,應該儘量避免使用Full GC。

4. Why:憑什麼說它是垃圾?——垃圾判斷演演算法

前面主要介紹了JVM垃圾回收機制的針對物件,GC分代思想和觸發條件。那麼,好好的一個物件範例,GC機制空口無憑憑什麼說它是垃圾呢?這就需要垃圾判斷演演算法了。
常見的垃圾判斷方法有兩種:參照計數法可達性分析法

4.1 參照計數法

  • 每個物件範例都有一個參照計數器,當有一個參照指向該物件範例時,參照計數器加1,當參照失效時,參照計數器減1,當參照計數器為0時,該物件範例就是垃圾,需要進行回收。
  • 補充一下JVM的參照型別,如下圖所示:
  • 優點:判斷邏輯簡單;
  • 缺點:無法解決迴圈參照的問題(從圖論角度來說就是環狀節點無法識別)。

4.2 可達性分析法

  • 將每個範例看作節點,兩個範例之間的參照關係看作路徑。從GC Roots開始,對堆記憶體中的物件進行遍歷,如果某個物件範例沒有被遍歷到,則說明該物件範例不可達,不可達則是垃圾,對其進行垃圾標記,等待後續回收(非連通圖查詢連通子圖的數量)。
  • GC Roots指的是正在執行的程式中一些基本物件,從這些物件往下查詢其參照物件,包括如下幾種:
    • 虛擬機器器棧(棧幀中的本地變數表)中參照的物件
    • 方法區中類靜態屬性參照的物件
    • 方法區中常數參照的物件
    • 本地方法棧中JNI(Native方法)參照的物件
  • 優點:能有效解決迴圈參照的問題;
  • 缺點:判斷邏輯複雜,需要遍歷整個記憶體空間,效率較低。

5. How:如何對待垃圾?——垃圾回收演演算法

常見的垃圾回收演演算法包括:標記-清除演演算法標記-整理演演算法複製演演算法分代收集演演算法(自適應演演算法)。

5.0 垃圾的垂死掙扎

在被處決之前,垃圾會進行一次垂死掙扎,範例第一次被標記為垃圾之後,如果可以進行一次有效finalize()方法呼叫,和其他範例建立參照,那麼該範例就會被複活,不會被回收。

5.1 標記-清除演演算法

在垃圾判斷演演算法執行完成後,已經被明確判斷成垃圾的範例,清除法在原地釋放其記憶體空間,將其標記為可用空間,等待後續的記憶體分配。

  • 優點:操作簡單,原地清除不需要複製記憶體
  • 缺點:會產生記憶體碎片,長期執行會影響CPU執行效率

5.2 標記-整理演演算法

標記-整理演演算法在標記完成之後,將所有存活的範例移動到一端,然後清除掉另一端的記憶體空間,這樣就可以有效解決記憶體碎片的問題。

  • 優點:無記憶體碎片;
  • 缺點:需要移動範例,效率較低;

5.3 複製演演算法

複製演演算法將記憶體空間分為兩塊,每次只使用其中一塊,當一塊記憶體空間記憶體滿了之後,將存活的範例複製到另一塊記憶體空間中,然後清除掉之前的記憶體空間。

  • 優點:無記憶體碎片,操作簡單;標記和複製可並行;
  • 缺點:可用記憶體空間直接減半,記憶體利用率較低;

5.4 分代收集演演算法

針對不同代的資料特點,使用不同的垃圾回收演演算法。

  • 年輕代:複製演演算法
    • 存活物件較少,複製演演算法每次的物件複製不會有太大負擔;
    • 操作頻繁,複製演演算法標記和複製可並行處理,效率較高;
  • 老年代:標記-整理演演算法
    • 存活物件較多,減少記憶體碎片,提高可用性
  • 永久代(元空間):不作考慮

6. Who:誰去處理垃圾?——垃圾回收器

垃圾回收器是垃圾回收演演算法的執行者,常見的垃圾回收器如下圖所示:

連線部分說明這兩個垃圾回收器能夠搭配使用。

6.1 年輕代-Serial收集器

  • 回收演演算法:複製演演算法
  • 單執行緒:執行回收演演算法的時候是單執行緒;適用於並行能力較低的系統。
  • Stop the World:一般執行緒和回收執行緒無法並行,執行回收演演算法需要中斷其他執行緒,這個現象稱為Stop the World(亂入Dio的「咋瓦魯多」)。
    • Stop the World現象會導致系統暫停,引出垃圾收集停頓時間這一引數,影響使用者體驗,因此需要儘量避免;
  • 優點:簡單高效,適用於單核CPU;
  • 缺點:無法並行,且單執行緒處理效率較低;
  • 啟用方式:-XX:+UseSerialGC

6.2 年輕代-ParNew收集器

  • 回收演演算法:複製演演算法
  • 多執行緒:執行回收演演算法的時候是多執行緒;
  • 也會存在Stop the World現象,除了多執行緒的改進之外,和Serial收集器沒有太大區別;
  • 優點:多執行緒處理效率高,適用於多核CPU;
  • 缺點:一般執行緒和回收執行緒無法並行處理;
  • 啟用方式:-XX:+UseParNewGC

6.3 年輕代-Parallel Scavenge收集器

  • 回收演演算法:複製演演算法
  • 關注吞吐量
    吞吐量 = 使用者執行緒執行時間 / (使用者執行緒執行時間 + 垃圾回收執行緒執行時間)
    
  • 相關引數
      1. 垃圾收集停頓時間
      • 設定方式:-XX:MaxGCPauseMillis=一個數值
      • 停頓時間過大將會直接影響每次垃圾回收的使用者體驗,停頓時間過小則會導致垃圾回收頻繁;
      1. 吞吐量大小
      • 設定方式:-XX:GCTimeRatio=一個數值
      • 預設取值為99%,表示只有1%的時間用於垃圾回收;
      1. 自適應模式
      • 設定方式:-XX:+UseAdaptiveSizePolicy
      • 設定自適應模式之後,記憶體中的新生代分配比例和老年代的時間引數可自行調整,達到吞吐量、停頓時間和記憶體佔用的平衡;
  • 是JDK1.8的預設垃圾回收器
  • 啟用方式:-XX:+UseParallelGC

6.4 老年代-SerialOld收集器

  • 回收演演算法:標記-整理演演算法
  • 單執行緒,可類比年輕代的Serial收集器,優缺點同理
  • CMS收集器的後備演演算法

6.5 老年代-ParallelOld收集器

  • 回收演演算法:標記-壓縮演演算法
  • 關注吞吐量,可類比年輕代的Parallel Scavenge收集器
  • 多執行緒,執行緒數通過-XX:ParallelGCThreads設定,預設為CPU核心數
  • 啟用方式:-XX:+UseParallelOldGC

6.6 老年代-CMS(Concurrent Mark Sweep)收集器

  • 回收演演算法:標記-清除演演算法
  • 關注停頓時間,期望有較短的垃圾回收停頓時間,從而優化使用者體驗;
  • 回收步驟:
    • 初始標記:適用可達性分析法的思想,標記GC Roots能直接關聯到的物件,速度較快;
    • 並行標記:一般執行緒和回收執行緒並行,對此時標記狀態出現變化的範例進行統計;
    • 重新標記:根據並行標記的結果,對標記狀態發生變化的範例進行重新標記,這一步相對較慢;
    • 並行清除:清理垃圾範例,釋放記憶體空間;這一步可以和一般執行緒並行;
  • 相關引數:
    • 觸發閾值:
      • 設定方式:-XX:CMSInitiatingOccupancyFraction=一個數值
      • 和之前討論的何時進行垃圾回收的觸發機制不同,CMS在處理老年代的時候,不會等到記憶體完全佔滿,而是會設定一個閾值,預設數值為68%,佔用記憶體空間超過這個閾值就進行垃圾回收處理。
    • 整理標記
      • 設定方式:-XX:+UseCMSCompactAtFullCollection
      • 如果設定這一標記,則垃圾回收之後會進行一次整理,合併記憶體碎片。
  • 優點:並行收集,提高執行效率;減少停頓時間,使用者體驗佳
  • 缺點:無法處理浮動垃圾,對CPU資源敏感,且標記清除演演算法會產生記憶體碎片

6.7 年輕代和老年代-G1(Garbage First)收集器

  • 回收演演算法:整體來看是標記-整理演演算法,區域性來看是複製演演算法
  • 關注停頓時間,也關注高吞吐量(我全都要.jpg);
  • 分割區:不同於之前所討論的分代劃分記憶體區域的方式,G1回收器將記憶體劃分為一個又一個單元區域,稱為Region
    • 設定方式:-XX:G1HeapRegionSize=一個數值
    • 每個分割區內部可以存放年輕代或者老年代的資料,根據不同的存放資料,將分割區劃分為四類:
      • E-Eden區:存放年輕代當中Eden區域的資料;
      • S-Survivor區:存放年輕代當中Survivor區域(survivor0和survivor1)的資料;
      • O-Old:存放老年代的一般資料;
      • H-Humongous:存放老年代當中大物件的資料;當佔據整個Region一半以上的時候,就會被劃分為Humongous區域;
  • 停頓預測模型:使用者可以自行設定垃圾回收停頓時間,而G1回收器會根據歷史資料構建預測模型,考慮為了滿足使用者設定的停頓時間,本次垃圾回收可以處理哪幾個Region。
  • Region優先順序佇列:G1瀏覽器維護一個Region佇列,高價值的Region有更高的優先順序,在垃圾回收的時候優先處理,這也是Garbage First名字的由來。
  • 回收步驟(和CMS回收器類似):
    • 初始標記
    • 並行標記
    • 重新標記
    • 並行清除
  • 優點:並行操作,並行收集,提高執行效率;關注停頓時間,可預測停頓時間,優化使用者體驗;可處理浮動垃圾,不會產生記憶體碎片;

6.8 垃圾回收器對比圖

對上述垃圾回收器的對比如下所示:

面試模擬

Q:介紹一下JVM和垃圾回收機制。
A:從"where"、"which"、"when"、"why"、"how"、"who"的角度,重點介紹觸發機制/判斷演演算法/垃圾回收演演算法/垃圾回收機制

參考資料

  1. JVM之垃圾回收機制(GC)
  2. JVM的垃圾回收機制 總結(垃圾收集、回收演演算法、垃圾回收器)
  3. 深入理解 JVM 垃圾回收機制及其實現原理