說明:在閱讀本篇之前,需要知道怎麼判斷對象的存活與否,見《第三章 JVM記憶體回收區域+對象存活的判斷+引用類型+垃圾回收線程》 註意:本文主要參考自《分散式Java應用:基礎與實踐》,與《深入理解Java虛擬機(第二版)》中的一些說法有一些不同,但是原理一致 1、三種垃圾回收演算法 標記-清除(年老代
說明:在閱讀本篇之前,需要知道怎麼判斷對象的存活與否,見《第三章 JVM記憶體回收區域+對象存活的判斷+引用類型+垃圾回收線程》
註意:本文主要參考自《分散式Java應用:基礎與實踐》,與《深入理解Java虛擬機(第二版)》中的一些說法有一些不同,但是原理一致
1、三種垃圾回收演算法
- 標記-清除(年老代)
- 標記-整理(即標記-壓縮)(年老代)
- 複製(年輕代)
1.1、標記-清除演算法
原理:
- 從根集合節點進行掃描,標記出所有的存活對象,最後掃描整個記憶體空間並清除沒有標記的對象(即死亡對象)
適用場合:
- 存活對象較多的情況下比較高效
- 適用於年老代(即舊生代)
缺點:
- 容易產生記憶體碎片,再來一個比較大的對象時(典型情況:該對象的大小大於空閑表中的每一塊兒大小但是小於其中兩塊兒的和),會提前觸發垃圾回收
- 掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
註意:
1.2、標記整理演算法
原理:
- 從根集合節點進行掃描,標記出所有的存活對象,最後掃描整個記憶體空間並清除沒有標記的對象(即死亡對象)(可以發現前邊這些就是標記-清除演算法的原理),清除完之後,將所有的存活對象左移到一起。
適用場合:
- 用於年老代(即舊生代)
缺點:
- 需要移動對象,若對象非常多而且標記回收後的記憶體非常不完整,可能移動這個動作也會耗費一定時間
- 掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
優點:
- 不會產生記憶體碎片
註意:
- 在該情況下,記憶體規整,對象的記憶體分配採用"指針碰撞法",見《第二章 JVM記憶體分配》
1.3、複製演算法
原理:
- 從根集合節點進行掃描,標記出所有的存活對象,並將這些存活的對象複製到一塊兒新的記憶體(圖中下邊的那一塊兒記憶體)上去,之後將原來的那一塊兒記憶體(圖中上邊的那一塊兒記憶體)全部回收掉
適用場合:
- 存活對象較少的情況下比較高效
- 掃描了整個空間一次(標記存活對象並複製移動)
- 適用於年輕代(即新生代):基本上98%的對象是"朝生夕死"的,存活下來的會很少
缺點:
- 需要一塊兒空的記憶體空間
- 需要複製移動對象
註意:
- 在該情況下,記憶體規整,對象的記憶體分配採用"指針碰撞法",見《第二章 JVM記憶體分配》
- 以空間換時間:通過一塊兒空記憶體的使用,減少了一次掃描
2、垃圾回收機制
根據《第一章 JVM記憶體結構》所說,年輕代分為Eden區和survivor區(兩塊兒:from和to),且Eden:from:to==8:1:1
1)新產生的對象優先分配在Eden區(除非配置了-XX:PretenureSizeThreshold,大於該值的對象會直接進入年老代);
2)當Eden區滿了或放不下了,這時候其中存活的對象會複製到from區(這裡,需要註意的是,如果存活下來的對象from區都放不下,則這些存活下來的對象全部進入年老代),之後Eden區的記憶體全部回收掉;註意:如果是Eden區沒有滿,但是來了一個小對象Eden區放不下,這時候Eden區存活對象複製到from區後,清空Eden區,之後剛纔的小對象再進入Eden區
3)之後產生的對象繼續分配在Eden區,當Eden區又滿了或放不下了,這時候將會把Eden區和from區存活下來的對象複製到to區(同理,如果存活下來的對象to區都放不下,則這些存活下來的對象全部進入年老代),之後回收掉Eden區和from區的所有記憶體;
4)如上這樣,會有很多對象會被覆制很多次(每複製一次,對象的年齡就+1),預設情況下,當對象被覆制了15次(這個次數可以通過:-XX:MaxTenuringThreshold來配置),就會進入年老代了
5)當年老代滿了或者存放不下將要進入年老代的存活對象的時候,就會發生一次Full GC(這個是我們最需要減少的,因為耗時很嚴重)
總結:
- 年輕代:複製演算法
- 年老代:標記-清除或標記-整理(前者相較於後者會快一些但是會產生記憶體碎片,後者相較於前者不會產生記憶體碎片但是由於要移動存活對象所以會慢一些)
- 以上這種年輕代與年老代分別採用不同回收演算法的方式稱為"分代收集演算法",這也是當下企業使用的一種方式
- 每一種演算法都會有很多不同的垃圾回收器去實現,在實際使用中,根據自己的業務特點做出選擇就好