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 記憶體細分
- 堆和方法區
堆中存放年輕代和老年代,而方法區中存放永久代。
- 新生代區域細分
新生代區域又分為Eden區
、Survivor0區
和Survivor1區
,比例為8:1:1
。
整體記憶體細分情況如圖所示:
3. When:什麼時候回收垃圾?——GC觸發條件
GC按照觸發條件,可分為Scavenge GC
和Full 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收集器
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"的角度,重點介紹觸發機制
/判斷演演算法
/垃圾回收演演算法
/垃圾回收機制
。
參考資料
- JVM之垃圾回收機制(GC)
- JVM的垃圾回收機制 總結(垃圾收集、回收演演算法、垃圾回收器)
- 深入理解 JVM 垃圾回收機制及其實現原理