Java垃圾收集演算法與垃圾收集器
- 1.垃圾收集的經典四連問
- 1.1. 什么是垃圾?
- 1.2.為什么要垃圾回收?
- 1.3. 垃圾什么時候回收?
- 1.4.垃圾如何回收?
- 2. 垃圾回收的相關概念
- 2.1 System.gc ()的理解
- 2.2 記憶體溢位與記憶體泄漏
- 2.3 Stop The World
- 2.4 垃圾回收的并發和并行
- 2.5 安全點與安全區域
- 2.6 五種參考
- 3. 垃圾回收相關演算法
- 3.1 垃圾標記階段演算法
- 3.2 物件的finalize機制
- 3.3 方法區的垃圾回收
- 3.4 垃圾清除階段演算法
- 3.5 分代收集演算法
- 3.6 增量收集演算法,磁區演算法
- 4. 經典的垃圾收集器
- 4.1 GC的分類與GC性能指標
- 4.2 不同垃圾收集器的概述
- 4.3 七款經典垃圾收集器介紹
- 4.4 七款經典垃圾收集器總結
以下文章內容來自周志明老師的深入理解Java虛擬機和看了尚硅谷康師傅的JVM教程之后的筆記,
1.垃圾收集的經典四連問
1.1. 什么是垃圾?
垃圾是指在運行的程式中沒有任何指標指向的物件,這個物件就是要被回收的垃圾,
1.2.為什么要垃圾回收?
-
如果不及時對記憶體中的垃圾進行清理,那么,這些垃圾物件所占的記憶體空間會一直保留到應用程式結束,被保留的空間無法被其他物件使用,甚至可能導致記憶體溢位,
-
除了釋放沒用的物件,垃圾回收也可以清除記憶體里的記錄碎片,碎片整理將所占用的堆記憶體移到堆的一端, 以便JVM將整理出的記憶體分配給新的物件,
-
隨著應用程式所應付的業務越來越龐大、復雜,用戶越來越多,沒有GC就不能保證應用程式的正常進行,而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化,
1.3. 垃圾什么時候回收?
如果某個物件已經不存在任何參考,那么它可以被回收,但是,具體到什么時刻執行,這個是由系統來進行決定,是無法預測的,
1.4.垃圾如何回收?
-
首先需要知道,GC又分為 minor GC 和 Full GC (也稱為 Major GC ),Java 堆記憶體分為新生代和老年代,新生代中又分為1個 Eden 區域 和兩個 Survivor 區域,
-
那么對于 Minor GC 的觸發條件:大多數情況下,直接在 Eden 區中進行分配,如果 Eden區域沒有足夠的空間,那么就會發起一次 Minor GC;對于 Full GC(Major GC)的觸發條件:也是如果老年代沒有足夠空間的話,那么就會進行一次 Full GC,
上面所說的只是一般情況下,實際上,需要考慮一個記憶體分配擔保的問題:
在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大于新生代所有物件的總空間,如果大于則進行Minor GC,如果小于則看HandlePromotionFailure設定是否允許擔保失敗(不允許則直接Full GC),如果允許,那么會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代物件的平均大小,如果大于則嘗試Minor GC(如果嘗試失敗也會觸發Full GC),如果小于則進行Full GC,
何為記憶體分配擔保?
記憶體的分配擔保好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,于是銀行可能會默認我們下一次也能按時按量地償還貸款,只需要有一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有什么風險了,記憶體的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活物件,這些物件便將通過分配擔保機制直接進入老年代,這對虛擬機來說就是安全的,
2. 垃圾回收的相關概念
2.1 System.gc ()的理解
在默認情況下,程式員可以通過System.gs ()或者Runtime. getRuntime() .gc()的呼叫,來顯式觸發FullGC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件占用的記憶體,而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過于麻煩了,在一些特殊情況下,如我們正在撰寫一個性能基準,我們可以在運行之間呼叫System.gc(),
public class systemGCTest {
public static void main(String[] args) {
new SystemGCTest();
//提醒jvm的垃圾回收器執行gc,但是不確定是否馬上執gc
System. gc();
//與Runtime. getRuntime().gc();的作用一樣,
System.runFinalization();//強制呼叫使用參考的物件的finalize()方法
@Override
protected void finalize() throws Throwable {
super.finalize();
System. out . println("SystemGCTest重寫了finalize()");
}
}
2.2 記憶體溢位與記憶體泄漏
一.記憶體溢位 :
javadoc中對outofMemoryError的解釋是,沒有空閑記憶體,并且垃圾收集器也無法提供更多記憶體,記憶體溢位是程式崩潰的罪魁禍首之一,
這個解釋中我們可以從兩個角度來看:
1.沒有空閑記憶體:
首先說沒有空閑記憶體的情況:說明Java虛擬機的堆記憶體不夠,原因有二:
(1) Java虛擬機的堆記憶體設定不夠,
比如:可能存在記憶體泄漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的資料量,但是沒有顯式指定JVM堆大小或者指定數值偏小,我們可以通過引數-Xms、-Xmx來調整堆記憶體大小,
(2)代碼中創建了大量大物件,并且長時間不能被垃圾收集器收集(存在被參考)
對于老版本的Oracle JDK Java1.8之前, 因為永久代的大小是有限的,并且JVM對永久代垃圾回收(如,常量池回收、卸載不再需要的型別)非常不積極,所以當我們不斷添加新型別的時候,永久代出現OutOfMemoryError也非常多見,尤其是在運行時存在大量動態型別生成的場合;類似intern字串快取占用太多空間,也會導致00M問題,對應的例外資訊,會標記出來和永久代相關:“java. lang . OutOfMemoryError: PermGen space",
隨著java1.8中元資料區的引入,方法區記憶體已經不再那么窘迫,所以相應的OOM有所改觀,出現OOM,例外資訊則變成了
“java.lang.OutOfMemoryError:Metaspace", 直接記憶體不足,也會導致OOM,
2.垃圾收集器也無法提供更多記憶體:
這里面隱含著一層意思是,在拋出0utOfMemoryError之前,通常垃圾收集器
會被觸發,盡其所能去清理出空間,
例如:在參考機制分析中,涉及到JVM會去嘗試回收軟參考指向的物件等,
在java. nio. BIts . reserveMemory( )方法中,我們能清楚的看到,System. gc()會被呼叫,以清理空間,
當然,也不是在任何情況下垃圾收集器都會被觸發的,比如,我們去分配一個超大物件,類似一個超大陣列超過堆的最大值,JVM可以判斷出垃圾收集并不能解決這個問題,所以直接拋OutOfMemoryError,
二.記憶體泄漏:
-
記憶體泄漏也稱作“存盤滲漏”,嚴格來說,只有物件不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體泄漏,
-
但實際情況很多時候–些不太好的實踐(或疏忽)會導致物件的生命周期變得很長甚至導致OOM,也可以叫做寬泛意義上的“記憶體泄漏”,
盡管記憶體泄漏并不會立刻引起程式崩潰,但是一旦發生記憶體泄漏,程式中的可用記憶體就會被逐步蠶食,直至耗盡所有記憶體,最終出現0utOfMemory例外,導致程式崩潰,
注意:這里的存盤空間并不是指物理記憶體,而是指虛擬記憶體大小,這個虛擬記憶體大小取決于磁盤交換區設定的大小,
2.3 Stop The World
-
stop- the-world,簡稱STW,指的是GC事件發生程序中,會產生應用程式的停頓,停頓產生時整個應用程式執行緒都會被暫停,沒有任何回應,有點像卡死的感覺,這個停頓稱為STW,
-
被STW中斷的應用程式執行緒會在完成GC之后恢復,頻繁中斷會讓用戶感覺像是網速不快造成電影卡帶-樣,所以我們需要減少STW的發生,
2.4 垃圾回收的并發和并行
并行(Parallel) :指多條垃圾收集執行緒并行作業,但此時用戶執行緒仍處于等待狀態,
串行(Serial): 相較于并行的概念,單執行緒執行,如果記憶體不夠,則程式暫停,啟動JVM垃圾回收器進行垃圾回收,回收完,再啟動程式的執行緒,

并發Concurrent) :指用戶執行緒與垃圾收集執行緒同時執行(但不一定是并行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓用戶程式的運行,用戶程式在繼續運行,而垃圾收集程式執行緒運行于另一個CPU上;

2.5 安全點與安全區域
安全點(Safepoint ) :程式執行時并非在所有地方都能停頓下來開始GC,只有在特定的位置才能停頓下來開始GC這些位置稱為安全點,
Safe Point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致運行時的性能問題,大部分指令的執行時間都非常短暫,通常會根據“是否具有讓程式長時間執行的特征”為標準,比如:選擇一些執行時間較長的指令作為Safe Point, 如方法呼叫、回圈跳轉和例外跳轉等,
問題來了如何在GC發生時,檢查所有執行緒都跑到最近的安全點停頓下來呢?
第一種解決辦法:搶先式中斷:(目前沒有虛擬機采用了)
首先中斷所有執行緒,如果還有執行緒不在安全點,就恢復執行緒,讓執行緒跑到安 全點,
第二種解決辦法:主動式中斷:
設定一個中斷標志,各個執行緒運行到Safe Point的時候主動輪詢這個標志, 如果中斷標志為真,則將自己進行中斷掛起,
Safepoint機制雖然保證了程式執行時,在不太長的時間內就會遇到可進入GC的Safepoint ,但是,程式“不執行”的時候呢?
例如執行緒處于Sleep 狀態或Blocked狀態,這時候執行緒無法回應JVM 的中斷請求,“走” 到安全點去中斷掛起,JVM也不太可能等待執行緒被喚醒,對于這種情況,就需要安全區域(Safe Region) 來解決,
安全區域:是指在一段代碼片段中,物件的參考關系不會發生變化,在這個區域中的任何位置開始GC都是安全的,我們也可以把Safe Region看做是被擴展了的Safepoint,
2.6 五種參考
無論是通過參考計數演算法判斷物件的參考數量,還是通過可達性分析演算法判斷物件是否參考鏈可達,判定物件是否存活都和“參考”離不開關系,
在JDK 1.2版之前,Java里面的參考是很傳統的定義:
如果reference型別的資料中存盤的數值代表的是另外一塊記憶體的起始地址,就稱該reference資料是代表某塊記憶體、某個物件的參考,這種定義并沒有什么不對,只是現在看來有些過于狹隘了,一個物件在這種定義下只有“被參考”或者“未被參考”兩種狀態,對于描述一些“食之無味,棄之可惜”的物件就顯 得無能為力,
譬如我們希望能描述一類物件:當記憶體空間還足夠時,能保留在記憶體之中,如果記憶體空間在進行垃圾收集后仍然非常緊張,那就可以拋棄這些物件,
在JDK 1.2版之后,Java對參考的概念進行了擴充,將參考分為
強參考(Strongly Re-ference)、軟參考(Soft Reference)、弱參考(Weak Reference)和虛參考(Phantom Reference)這4種參考強度依次逐漸減弱,
2.6.1 強參考介紹:
強參考:是最傳統的“參考”的定義,是指在程式代碼之中普遍存在的參考賦值,即類似“Object obj=new Object()”這種參考關系,無論任何情況下,只要強參考關系還存在,垃圾收集器就永遠不會回收掉被參考的物件,
在Java程式中,最常見的參考型別是強參考(普通系統99%以上都是強參考),也就是我們最常見的普通物件參考,也是默認的參考型別,當在Java語言中使用new運算子創建一個新的物件, 并將其賦值給個變數的時候,這個變數就成為指向該物件的一個強參考,
強參考的物件是可觸及的,垃圾收集器就永遠不會回收掉被參考的物件,
對于一個普通的物件,如果沒有其他的參考關系,只要超過了參考的作用域或者顯式地將相應(強)參考賦值為null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略,
相對的軟參考、 弱參考和虛參考的物件是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的,所以,強參考是造成Java記憶體泄漏的主要原因之一,
強參考特點:
-
強參考可以直接訪問目標物件,
-
強參考所指向的物件在任何時候都不會被系統回收,虛擬機寧愿拋出OOM例外,也不會回收強參考所指向物件,
-
強參考可能導致記憶體泄漏,
2.6.2 軟參考介紹:
軟參考:是用來描述一些還有用,但非必須的物件,只被軟參考關聯著的物件,在系統將要發生記憶體溢位例外前,會把這些物件列進回收范圍之中進行第二次回收,如果這次回識訓沒有足夠的記憶體,才會拋出記憶體溢位例外,
軟參考作用: 軟參考通常用來實作記憶體敏感的快取,比如:高速快取就有用到軟參考,如果還有空閑記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體,
什么時候清理 :垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟參考,并可選地把參考存放到一個參考佇列( Reference Queue),類似弱參考,只不過Java虛擬機會盡量讓軟參考的存活時間長一些,迫不得已才清理,
在JDK 1.2版之后提供了SoftReference類來實作軟參考,
Object obj = new 0bject(); //宣告強參考
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; //銷毀強參考
2.6.3 弱參考介紹:
弱參考:也是用來描述那些非必須物件,但是它的強度比軟參考更弱一些,被弱參考關聯的物件只能生存到下一次垃圾收集發生為止,當垃圾收集器開始作業,無論當前記憶體是否足夠,都會回收掉只被弱參考關聯的物件,
但是,由于垃圾回收器的執行緒通常優先級很低,因此,并不一定能很快地發現持有弱參考的物件,在這種情況下,弱參考物件可以存在較長的時間,
弱參考和軟參考一樣,在構造弱參考時,也可以指定一個參考佇列,當弱參考物件被回收時,就會加入指定的參考佇列,通過這個佇列可以跟蹤物件的回收情況,
軟參考的作用:軟參考、弱參考都非常適合來保存那些可有可無的快取資料,如果這么做,當系統記憶體不足時,這些快取資料會被回收,不會導致記憶體溢位,而當記憶體資源,充足時,這些快取資料又可以存在相當長的時間,從而起到加速系統的作用,
在JDK 1.2版之后提供了WeakReference類來實作弱參考,
object obj = new Object(); / /宣告強參考
WeakReference<Object> wr = new WeakReference<Object>(obj );
obj = null; //銷毀強參考
弱參考物件與軟參考物件的區別:
弱參考物件與軟參考物件的最大不同就在于,當GC在進行回收時,需要通過
演算法檢查是否回收軟參考物件,而對于弱參考物件,GC總是進行回收,弱引
用物件更容易、更快被GC回收,
2.6.4 虛參考介紹:
虛參考:也稱為“幽靈參考”或者“幻影參考”,它是最弱的一種參考關系,一個物件是否有虛參考的存在,完全不會對其生存時間構成影響,也無法通過虛參考來取得一個物件實體,為一個物件設定虛參考關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知,
-
虛參考必須和參考佇列一起使用,虛參考在創建時必須提供-一個參考佇列作為引數,當垃圾回收器準備回收–個物件時,如果發現它還有虛參考,就會在回收物件后,將這個虛參考加入參考佇列,以通知應用程式物件的回收情況,
-
由于虛參考可以跟蹤物件的回收時間,因此,也可以將–些資源釋放操作
放置在虛參考中執行和記錄,
在JDK 1.2版之后提供了PhantomReference類來實作虛參考,
object obj = new 0bject();
ReferenceQueue phantomQueue = new ReferenceQueue( );
PhantomReference<0bject> pf = new PhantomReference<object> (obj, phantomQueue);
obj = null;
2.6.5 終結器參考(Final reference)
- 它用以實作物件的finalize()方法,也可以稱為終結器參考,
- 無需手動編碼,其內部配合參考佇列使用,
- 在GC時,終結器參考入隊,由Finalizer執行緒通過終結器參考找到被參考
物件并呼叫它的finalize()方法,第二次GC時才能回收被參考物件,
3. 垃圾回收相關演算法
3.1 垃圾標記階段演算法
在堆里存放著幾乎所有的Java物件實體,在GC執行垃圾回收之前,首先需
要區分出記憶體中哪些是存活物件,哪些是已經死亡的物件,只有被標記為己
經死亡的物件,GC才會在執行垃圾回收時,釋放掉其所占用的記憶體空間,因
此這個程序我們可以稱為垃圾標記階段,
那么在JVM中究竟是如何標記一個 死亡物件呢?簡單來說,當一個物件已經不再被任何的存活物件繼續參考時,就可以宣判為已經死亡,
3.1.1參考計數演算法
參考計數演算法(Reference Counting) 比較簡單,對每個物件保存一個 整型的參考計數器屬性,用于記錄物件被參考的情況,
對于一個物件A,只要有任何一個物件參考了A,則A的參考計數器就加1;當參考失效時,參考計數器就減1,只要物件A的參考計數器的值為0,即表示物件A不可能再被使用,可進行回收,
優點:實作簡單,垃圾物件便于辨識;判定效率高,回收沒有延遲性,
缺點:
- 它需要單獨的欄位存盤計數器,這樣的做法增加了存盤空間的開銷,
- 每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷,
- 參考計數器有一個嚴重的問題,即無法處理回圈參考的情況,這是一條致命 缺陷,導致在Java的垃圾回收器中沒有使用這類演算法,
記憶體泄漏
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 這個成員屬性的唯一意義就是占點記憶體,以便能在GC日志中看清楚是否有回收過
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假設在這行發生GC,objA和objB是否能被回收?
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
GC(0) Pause Full (System.gc()) 9M->0M(10M) 5.293ms
從運行結果中可以清楚看到記憶體回收日志中包含“ GC(0) Pause Full (System.gc()) 9M->0M(10M) 5.293ms”,意味著虛擬機并沒有因為這兩個物件互相參考就放棄回收它們,這也從側面說明了Java虛擬機并不是通過參考計數演算法來判斷物件是否存活的
3.1.2可達性分析演算法
基本思路:
- 可達性分析演算法是以根物件集合(GC Roots) 為起始點,按照從上至下
的方式搜索被根物件集合所連接的目標物件是否可達, - 使用可達性分析演算法后,記憶體中的存活物件都會被根物件集合直接或間
接連接著,搜索所走過的路徑稱為參考鏈(Re ference Chain) - 如果目標物件沒有任何參考鏈相連,則是不可達的,就意味著該物件己
經死亡,可以標記為垃圾物件, - 在可達性分析演算法中,只有能夠被根物件集合直接或者間接連接的物件
才是存活物件,

所謂"GC Roots "根集合就是一組必須活躍的參考,
在Java技術體系里面,固定可作為GC Roots的物件包括以下幾種:
- 在虛擬機堆疊(堆疊幀中的本地變數表)中參考的物件,譬如各個執行緒被呼叫的方法堆疊中使用到的引數、區域變數、臨時變數等,
- 在方法區中類靜態屬性參考的物件,譬如Java類的參考型別靜態變數,
- 在方法區中常量參考的物件,譬如字串常量池(String Table)里的參考,
- 在本地方法堆疊中JNI(即通常所說的Native方法)參考的物件,
- Java虛擬機內部的參考,如基本資料型別對應的Class物件,一些常駐的例外物件(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器,
- 所有被同步鎖(synchronized關鍵字)持有的物件,
- 反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回呼、本地代碼快取等,
除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的記憶體區域不同,還可以有其他物件“臨時性”地加入,共同構成完整GC Roots集合,
譬如分代收集和區域回收(Partial GC),如果只針對Java堆中某一塊區域發起垃圾收集時(如最典型的只針對新生代的垃圾收集),必須考慮到記憶體區域是虛擬機自己的實作細節(在用戶視角里任何記憶體區域都是不可見的),更不是孤立封閉的,所以某個區域里的物件完全有可能被位于堆中其他區域的物件所參考,這時候就需要將這些關聯區域的物件也一并加入GC Roots集合中去,才能保證可達性分析的正確性,
由于Root采用堆疊方式存放變數和指標,所以如果一個指標,它保存了堆記憶體
里面的物件,但是自己又不存放在堆記憶體里面,那它就是一個Root
如果要使用可達性分析演算法來判斷記憶體是否可回收,那么分析作業必須在
一個能保障一致性的快照中進行,這點不滿足的話分析結果的準確性就無
法保證,
3.2 物件的finalize機制
Java語言提供了物件終止(finalization) 機制來允許開發人員提供物件被銷毀之前的自定義處理邏輯,
當垃圾回收器發現沒有參考指向一個物件,即:垃圾回收此物件之前,總會
先呼叫這個物件的finalize()方法,
finalize()方法允許在子類中被重寫,用于在物件被回收時進行資源釋放,通常在這個方法中進行一些資源釋放和清理的作業,比如關閉檔案、套接字和資料庫連接等,
永遠不要主動呼叫某個物件的finalize()方法,應該交給垃圾回識訓制呼叫,理由包括下面幾點:
- 在finalize() 時可能會導致物件復活,
- finalize() 方法的執行時間是沒有保障的,它完全由GC執行緒決定,極端情況下,若不發生GC,則finalize() 方法將沒有執行機會,
- 一個糟糕的finalize() 會嚴重影響GC的性能 ,
- 從Java9開始該方法已經被標識為過時方法了
@Deprecated(since="9") protected void finalize() throws Throwable { }
從功能上來說,finalize ()方法與C+ +中的解構式比較相似,但是Java采用的是基于垃圾回收器的自動記憶體管理機制,所以finalize ()方法在本質上不同于C++中的解構式,
由于finalize ()方法的存在,虛擬機中的物件一般處于三種可能的狀態,
如果從所有的根節點都無法訪問到某個物件,說明物件已經不再使用了,一般來說,此物件需要被回收,但事實上,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,一個無法觸及的物件有可能在某-一個條件下“復活”自己,如果這樣,那么對它的回收就是不合理的,為此,定義虛擬機中的物件可能的三種狀態,如下:
可觸及的: 從根節點開始,可以到達這個物件,
可復活的: 物件的所有參考都被釋放,但是物件有可能在finalize()中復活,
不可觸及的: 物件的finalize()被呼叫,并且沒有復活,那么就會進入不可觸及狀態, 不可觸及的物件不可能被復活,因為finalize() 只會被呼叫一次,
以上3種狀態中,是由于finalize ()方法的存在,進行的區分,只有在物件不可觸及時才可以被回收,
判定一個物件objA是否可回收,至少要經歷兩次標記程序:
1.如果物件objA到GC Roots沒有參考鏈,則進行第一次標記,.
2.進行篩選,判斷此物件是否有必要執行finalize()方法
①如果物件objA沒有 重寫finalize()方法,或者finalize() 方法已經被虛擬機呼叫過,則虛擬機視為“沒有必要執行”,objA被判定為不可觸及的,
②如果物件objA重寫 了finalize()方法,且還未執行過,那么objA會 被插入到F-Queue佇列中,由一個虛擬機自動創建的、低優先級的Finalizer執行緒觸發其finalize()方法執行,
③finalize() 方法是物件逃脫死亡的最后機會,稍后Gc會對F-Queue佇列中的物件進行第二次標記,如果objA在finalize()方法中與參考鏈上的任何一-個物件建立了聯系,那么在第二次標記時,objA會被移出“即將回收”集合,之后,物件會再次出現沒有參考存在的情況,在這個情況下,finalize方法不會被再次呼叫,物件會直接變成不可觸及的狀態,也就是說,一個物件的finalize方法只會被呼叫一次,
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :()");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//物件第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因為Finalizer方法優先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :()");
}
// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了
SAVE_HOOK = null;
System.gc();
// 因為Finalizer方法優先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :()");
}
}
}
finalize method executed!
yes, i am still alive :()
no, i am dead :()
從代碼中我們可以看到一個物件的finalize()被執行,但是它仍然可以存活,
另外一個值得注意的地方就是,代碼中有兩段完全一樣的代碼片段,執行結果卻是一次逃脫成功,一次失敗了,這是因為任何一個物件的finalize()方法都只會被系統自動呼叫一次,如果物件面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了,
3.3 方法區的垃圾回收
方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的型別,
回收廢棄常量與回收Java堆中的物件非常類似,舉個常量池中字面量回收的例子,假如一個字串“java”曾經進入常量池中,但是當前系統又沒有任何一個字串物件的值是“java”,換句話說,已經沒有任何字串物件參考常量池中的“java”常量,且虛擬機中也沒有其他地方參考這個字面量,如果在這時發生記憶體回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統清理出常量池,常量池中其他類(介面)、方法、欄位的符號參考也與此類似,
判定一個常量是否“廢棄”還是相對簡單,而要判定一個型別是否屬于“不再被使用的類”的條件就比較苛刻了,需要同時滿足下面三個條件:
- 該類所有的實體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實體,
- 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的,
- 該類對應的java.lang.Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,
Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和物件一樣,沒有參考了就必然會回收,關于是否要對型別進行回收,HotSpot虛擬機提供了-Xnoclassgc 引數進行控制,以下引數查看類加載和卸載資訊:
-verbose:class
-XX:+TraceClass-Loading
-XX:+TraceClassUnLoading
在大量使用反射、動態代理、CGLib等位元組碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備型別卸載的能力,以保證不會對方法區造成過大的記憶體壓力,
注意在JDK1.6之前和之后方法區進行了如下調整:

3.4 垃圾清除階段演算法
當成功區分出記憶體中存活物件和死亡物件后,GC接下來的任務就是執行垃圾
回收,釋放掉無用物件所占用的記憶體空間,以便有足夠的可用記憶體空間為新物件分配記憶體,
目前在JVM中比較常見的三種垃圾收集演算法是標記-清除演算法( Mark-Sweep)、復制演算法(Copying)、標記一壓縮演算法(Mark-Compact ),
3.4.1 標記-清除演算法( Mark-Sweep)
最早出現也是最基礎的垃圾收集演算法是“標記-清除”(Mark-Sweep)演算法,在1960年由Lisp之父John McCarthy所提出,如它的名字一樣,演算法分為“標記”和“清除”兩個階段:
標記: Collector從參考根 節點開始遍歷,標記所有被參考的物件,一般是 在物件的Header中記錄為可達物件,
清除: Collector對堆記憶體從頭到尾進行線性的遍歷,如果發現某個物件在其Header中沒有標記為可達物件,則將其回收,
注意:這里深入理解java虛擬機這本書中清除階段描述的是:
首先標記出所有需要回收的物件,在標記完成后,統一回收掉所有被標記的物件,也可以反過來,標記存活的物件,統一回收所有未被標記的物件,
標記-清除演算法的執行程序如圖所示:


它的主要缺點有兩個:
第一個是執行效率不穩定,如果Java堆中包含大量物件,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個程序的執行效率都隨物件數量增長而降低;
第二個是記憶體空間的碎片化問題,標記、清除之后會產生大量不連續的記憶體碎片,空間碎片太多可能會導致當以后在程式運行程序中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,需要維護一個空閑串列,
所以這里所謂的清除并不是真的置空,而是把需要清除的物件地址保存在空閑的地址串列里,下次有新物件需要加載時,判斷垃圾的位置空間是否夠, 如果夠,就存放,
3.4.2 復制演算法(Copying)
核心思想:
將活著的記憶體空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在
使用的記憶體中的存活物件復制到未被使用的記憶體塊中,之后清除正在使用
的記憶體塊中的所有物件,交換兩個記憶體的角色,最后完成垃圾回收,


優點:
- 沒有標記和清除程序,實作簡單,運行高效
- 復制過去 以后保證空間的連續性,不會出現“碎片”問題,
缺點:
- 此演算法的缺點也是很明顯的,就是需要兩倍的記憶體空間,
- 對于G1這種分拆成為大量region的GC,復制而不是移動,意味著GC需要維護region之間物件參考關系,不管是記憶體占用或者時間開銷也不小,
- 如果系統中的垃圾物件很多,復制演算法需要復制的存活物件數量并不會太大,或者說非常低才行,
應用場景:
在新生代,對常規應用的垃圾回收,一次通常可以回收70號- 99號的記憶體空間,回收性價比很高,所以現在的商業虛擬機都是用這種收集演算法回收新生代,

3.4.3 標記一壓縮(整理)演算法(Mark-Compact )
標記-復制演算法在物件存活率較高時就要進行較多的復制操作,效率將會降低,更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法,正是因為這個原因所以標記壓縮演算法誕生了,
標記一壓縮演算法執行程序:
第一階段:和標記清除演算法一樣,從根節點開始標記所有被參考物件,
第二階段:將所有的存活物件壓縮到記憶體的一端,按順序排放之后,清理邊界外所有的空間,

標記-壓縮演算法的最終效果等同于標記-清除演算法執行完成后,再進行一次
記憶體碎片整理,因此,也可以把它稱為標記-清除-壓縮(Mark- Sweep-
Compact)演算法,
二者的本質差異在于標記-清除演算法是一種非移動式的回收演算法,標記-壓
縮是移動式的,是否移動回收后的存活物件是一項優缺點并存的風險決策,
為什么說是優缺點并存呢?原因有如下兩點
- 如果移動存活物件,尤其是在老年代這種每次回收都有大量物件存活區域,移動存活物件并更新所有參考這些物件的地方將會是一種極為負重的操作,而且這種物件移動操作必須全程暫停用戶應用程式才能進行也就是必須stop-the-world,
- 如果跟標記-清除演算法那樣完全不考慮移動和整理存活物件的話,彌散于堆中的存活物件導致的空間碎片化問題就只能依賴更為復雜的記憶體分配器和記憶體訪問器來解決,譬如通過“磁區空閑分配鏈表”來解決記憶體分配問題(計算機硬碟存盤大檔案就不要求物理連續的磁盤空間,能夠在碎片化的硬碟上存盤和訪問就是通過硬碟磁區表實作的),
基于以上兩點,是否移動物件都存在弊端,移動則記憶體回收時會更復雜,不移動則記憶體分配時會更復雜,從垃圾收集的停頓時間來看,不移動物件停頓時間會更短,甚至可以不需要停頓,但是從整個程式的吞吐量來看,移動物件會更劃算,
有一種“和稀泥式”解決方案可以不在記憶體分配和訪問上增加太大額外負擔,做法是讓虛擬機平時多數時間都采用標記-清除演算法,暫時容忍記憶體碎片的存在,直到記憶體空間的碎片化程度已經大到影響物件分配時,再采用標記-整理演算法收集一次,以獲得規整的記憶體空間,其中CMS收集器面臨空間碎片過多時采用的就是這種處理辦法,
指標碰撞(Bump the Pointer)
如果記憶體空間以規整和有序的方式分布,即已用和未用的記憶體都各自一邊,彼此之間維系著一個記錄下一次分配起始點的標記指標,當為新物件分配記憶體時,只需要通過修改指標的偏移量將新物件分配在第一個空閑記憶體位置上,這種分配方式就叫做指標碰撞(Bump tHe Pointer) ,
優點:
- 消除了標記-清除演算法當中,記憶體區域分散的缺點,我們需要給新物件分配記憶體時,JVM只需要持有一個記憶體的起始地址即可,
- 消除了復制演算法當中,記憶體減半的高額代價,
缺點:
- 從效率上來說,標記- -整理演算法要低于復制演算法,
- 移動物件的同時,如果物件被其他物件參考,則還需要調整參考的地址,
- 移動程序中,需要全程暫停用戶應用程式,即: STW
三種演算法對比
| Mark-Sweep | Mark-Compact | Copying | |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空間開銷 | 少(但會堆積碎片) | 少(不地積碎片) | 通常需要活物件的2倍大小(不堆積碎片) |
| 是否移動物件 | 否 | 是 | 是 |
3.5 分代收集演算法
前面所有這些演算法中,并沒有一種演算法可以完全替代其他演算法,它們都具有自己獨特的優勢和特點,分代收集演算法應運而生,
分代收集演算法:是基于這樣一個事實:不同的物件的生命周期是不一樣的,因此,不同生命周期的物件可以采取不同的收集方式,以便提高回收效率,一 般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收演算法,以提高垃圾回收的效率,
在Java程式運行的程序中,會產生大量的物件,其中有些物件是與業務資訊相關,比如Http請求中的Session物件、執行緒、Socket連接,這類物件跟業務
直接掛鉤,因此生命周期比較長,
但是還有一些物件,主要是程式運行程序中生成的臨時變數,這些物件生命周期會比較短,比如: string物件, 由于其不變類的特性,系統會產生大量的這些物件,有些物件甚至只用一次即可回收,
目前幾乎所有的GC都是采用分代收集(Generational Collecting) 演算法執行垃圾回收的,
在HotSpot中,基于分代的概念,GC所使用的記憶體回收演算法必須結合年輕代和老年代各自的特點,
年輕代(Young Gen)
年輕代特點:區域相對老年代較小,物件生命周期短、存活率低,回收頻 繁,
這種情況復制演算法的回收整理,速度是最快的,復制演算法的效率只和當前存活物件大小有關,因此很適用于年輕代的回收,而復制演算法記憶體利用率不高的問題,通過hotspot中 的兩個survivor的設計得到緩解,
老年代(Tenured Gen)
老年代特點: 區域較大,物件生命周期長、存活率高,回收不及年輕代頻繁,
這種情況存在大量存活率高的物件,復制演算法明顯變得不合適,一般是由標記-清除或者是標記-清除與標記-整理的混合實作,
- Mark階段的開銷與存活物件的數量成正比,
- sweep階段的開銷與所管理區域的大小成正相關,
- Compact階段的開銷與存活物件的資料成正比,
3.6 增量收集演算法,磁區演算法
增量收集演算法
上述現有的演算法,在垃圾回收程序中,應用軟體將處于一種Stop the World
的狀態,在Stop the World 狀態下,應用程式所有的執行緒都會掛起,暫停一
切正常的作業,等待垃圾回收的完成,如果垃圾回收時間過長,應用程式會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性,為了解決這個問題,即對實時垃圾收集演算法的研究直接導致了增量收集( Incremental Collecting) 演算法的誕生,
基本思想
如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集執行緒和應用程式執行緒交替執行,每次,垃圾收集執行緒只收集一小片區域的記憶體空間,接著切換到應用程式執行緒,依次反復,直到垃圾收集完成,總的來說,增量收集演算法的基礎仍是傳統的標記-清除和復制演算法,增量收集演算法通過對執行緒間沖突的妥善處理,允許垃圾收集執行緒以分階段的方式完成標記、清理或復制作業,
缺點:
使用這種方式,由于在垃圾回收程序中,間斷性地還執行了應用程式代碼,
所以能減少系統的停頓時間,但是,因為執行緒切換和背景關系轉換的消耗,會
使得垃圾回收的總體成本上升,造成系統吞吐量的下降,
磁區演算法
一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間就越長,有關GC產生的停頓也越長,為了更好地控制Gc產生的停頓時間,將一塊大的記憶體區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓,
分代演算法將按照物件的生命周期長短劃分成兩個部分,磁區演算法將整個堆空間劃分成連續的不同小區間,每一個小區間都獨立使用,獨立回收,這種演算法的好處是可以控制一次回收多少個小區間,

4. 經典的垃圾收集器
4.1 GC的分類與GC性能指標
4.1.1GC的分類
垃圾收集器沒有在規范中進行過多的規定,可以由不同的廠商、不同版本
的JVM來實作,
由于JDK的版本處于高速迭代程序中,因此Java發展至今已經衍生了眾多
的GC版本,從不同角度分析垃圾收集器,可以將GC分為以下不同的型別,
- 按照執行緒數來分,可以分為串行垃圾收集器和并行垃圾收集器

串行回收:指的是在同一時間段內只允許有一個CPU用于執行垃圾回收操作,此時作業執行緒被暫停,直至垃圾收集作業結束,
在并發能力比較強的CPU上,并行回收器產生的停頓時間要短于串行回收器,
在諸如單CPU處理器或者較小的應用記憶體等硬體平臺不是特別優越的場合,串行回收器的性能表現可以超過并行回收器和并發回收器,所以,串行回收默認被應用在客戶端的Client模式下的JVM中
hotspot虛擬機中包括server和client兩種模式:
1. Java HotSpot Client VM:為在客戶端環境中減少啟動時間而優化;比較適合桌面程式,它會做一些例如像快速初始化,懶加載這一類的事件來適應桌面程式的特點,
2. Java HotSpot Server VM:為在服務器環境中最大化程式執行速度而設計; 適合做服務器程式,一些針對服務器特點的事情,比如預加載,尤其在一些并發的處理上,是會做更多的優化,
不過,默認情況下32位的hotspot都是client模式;64位的的都是server模式,
并行回收:和串行回收相反,并行收集可以運用多個CPU同時執行垃圾回收,因此提升了應用的吞吐量,不過并行回收仍然與串行回收一樣,采用獨占式,使用了“ Stop-the-world”機制,
2.按照作業模式分為并發式和獨占式
并發式:并發式垃圾回收器與應用程式執行緒交替作業,以盡可能減少應用程式的停頓時間,
獨占式:獨占式垃圾回收器(Stop the world)一 旦運行,就停止應用程式中的所有用戶執行緒,直到垃圾回收程序完全結束,
3.按碎片處理方式分,可分為壓縮式垃圾回收器和非壓縮式垃圾回收器,
壓縮式垃圾回收器會在回收完成后,對存活物件進行壓縮整理,消除回收后的碎片,
非壓縮式的垃圾回收器不進行這步操作,
4.按作業的記憶體區間分,又可分為年輕代垃圾回收器和老年代垃圾回收器,
4.1.2 GC的性能評估
吞吐量:運行用戶代碼的時間占總運行時間的比例
(總運行時間:程式的運行時間十記憶體回收的時間)
吞吐量=運行用戶代碼的時間/(程式的運行時間+記憶體回收的時間)
比如:虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%,
高吞吐量的應用程式有更長的時間基準,應用程式能容忍較高的暫停時間,快速回應是不必考慮的,

吞吐量優先,意味著在單位時間內,STW的時間最短: 0.2 + 0.2 = 0.4
垃圾收集開銷: 吞吐量的補數,垃圾收集所用時間與總運行時間的比例,
暫停時間: 執行垃圾收集時,程式的作業執行緒被暫停的時間,是指一個時間段內應用程式執行緒暫停,讓GC執行緒執行的狀態,
例如,GC期間100毫秒的暫停時間意味著在這100毫秒期間內沒有應用 程式執行緒是活動的,

暫停時間優先,意味著盡可能讓單次STW的時間最短:
0.1 + 0.1+ 0.1 +0.1+0.1=0.5
收集頻率: 相對于應用程式的執行,收集操作發生的頻率,
記憶體占用: Java堆區所占的記憶體大小,
快速: 一個物件從誕生到被回收所經歷的時間,
上述的所有性能評估中吞吐量,暫停時間,和記憶體占用這三者共同構成一個“不可能三角”,
三者總體的表現會隨著技術進步而越來越好,一款優秀的收集器通常最多同時滿足其中的兩項,
這三項里,暫停時間的重要性日益凸顯,因為隨著硬體發展,記憶體占用多些越來越能容忍,硬體性能的提升也有助于降低收集器運行時對應用程式的影響,即提高了吞吐量,而記憶體的擴大,對延遲反而帶來負面效果,因為記憶體擴大了垃圾收集的記憶體就更多了,
總的來說,主要抓住吞吐量和暫停時間這兩點既可,
高吞吐量 較好因為這會讓應用程式的最終用戶感覺只有應用程式執行緒在做“生產性”作業,直覺上,吞吐量越高程式運行越快,
低暫停時間 (低延遲)較好因為從最終用戶的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的,這取決于應用程式的型別,有時候甚至短暫的200毫秒暫停都可能打斷終端用戶體驗,因此,具有低的較大暫停時間是非常重要的,特別是對于一個互動式應用程式,
不幸的是”高吞吐量”和”低暫停時間”是一對相互競爭的目標(矛盾),
因為如果選擇以吞吐量優先,那么必然需要降低記憶體回收的執行頻率,但是這樣會導致GC需要更長的暫停時間來執行記憶體回收,
相反的,如果選擇以低延遲優先為原則,那么為了降低每次執行記憶體回收時的暫停時間,也只能頻繁地執行記憶體回收,但這又引起了年輕代記憶體的縮減和導致程式吞吐量的下降,
在設計GC演算法時保證在最大吞吐量優先的情況下,降低停頓時間,
4.2 不同垃圾收集器的概述
發展史
1999年隨JDK1.3.1一起來的是串行方式的Serial GC,它是第一款GC, ParNew垃圾收集器是Serial收集器的多執行緒版本,
2002年2月26日,Parallel GC和Concurrent Mark Sweep GC跟隨JDK1.4.2一
起發布,
Parallel GC在JDK6之 后成為HotSpot默認GC,
2012年,在JDK1. 7u4版本中,G1可用,
2017年,JDK9中G1變成默認的垃圾收集器,以替代CMS,
2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,實作并行性來改善最壞情況下的延遲,
2018年9月,JDK11發布,引入Epsilon垃圾回收器,又被稱為"No-0p (無操作) "回收器,同時,引入ZGC:可伸縮的低延遲垃圾回收器(Experimental),
2019年3月,JDK12發布,增強G1,自動回傳未用堆記憶體給作業系統,同時,引入Shenandoah GC:低停頓時間的GC (Experimental),
2019年9月,JDK13發布,增強ZGC,自動回傳未用堆記憶體給作業系統,
2020年3月,JDK14發布,洗掉CMS垃圾回收器,擴展ZGC在macOS和Windows上的應用
七款經典垃圾收集器

七款經典垃圾收集器與垃圾分代之間的關系

垃圾收集器的組合關系

-
兩個收集器間有連線,表明它們可以搭配使用:
Serial/Serial old、Seria1/CMS、 ParNew/Serial old、ParNew/CMS、Parallel Scavenge/Serial old、Parallel Scavenge/Parallel old、G1; -
其中Serial old作為CMS出現"Concurrent Mode Failure"失敗的后 備預案,
-
(紅色虛線)由于維護和兼容性測驗的成本,在JDK 8時將Serial+CMS、
ParNew+Serial old這兩個組合宣告為廢棄,并在JDK 9中完全取消了這些組合的支持,即:移除, -
(綠色虛線)JDK 14中:棄用Parallel Scavenge和Serialold GC組合
-
(青色虛線)JDK 14中:洗掉CMS垃圾回收器
因為Java的使用場景很多,移動端,服務器等,所以就需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的性能,沒有一種放之四海皆準、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器,所以我們要選擇的只是對具體應用最合適的收集器,
-xx:+PrintCommandLineFlags: 查看命令列相關引數(包含使用的垃圾收集器)
使用命令列指令: jinfo -flag 相關垃圾回收器引數行程ID
4.3 七款經典垃圾收集器介紹
4.3.1 Serial垃圾收集器:串行回收
Serial垃圾收集器介紹
Serial收集器是最基本、歷史最悠久的垃圾收集器了,JDK1.3之前回收新生代唯一的選擇,
Serial收集器作為HotSpot中Client模式下的默認新生代垃圾收集器,
Serial收集器采用復制演算法、串行回收和stop- the-World機制的方式執行記憶體回收,
除了年輕代之外,Serial收集器還提供用于執行老年代垃圾收集的Serial old收集器,
Serial Old收集器同樣也采用了串行回收和"Stop the World"機制,只不過記憶體回收演算法使用的是標記-壓縮演算法,
Serial old是運行在Client模式下默認的老年代的垃圾回收器
Serial old在Server模式下 主要有兩個用途:
- 與新生代的Parallel Scavenge配合使用
- 作為老年代CMS收集器的后備垃圾收集方案
Serial收集器運行示意圖

這個收集器是一個單執行緒的收集器,但它的“單執行緒”的意義并不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集作業,更重要的是在它進行垃圾收集時,必須暫停其他所有的作業執行緒,直到它收集結束(Stop The World) ,
Serial的應用場景
優勢:簡單而高效(與其他收集器的單執行緒比),對于限定單個CPU 的環境來說,Serial收集器由于沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率,運行在Client模式下的虛擬機是個不錯的選擇,
在用戶的桌面應用場景中,可用記憶體一 般不大(幾十MB至一兩百MB),
可以在較短時間內完成垃圾收集(幾十ms至一百多ms) , 只要不頻繁發生,使用串行回收器是可以接受的,
在HotSpot虛擬機中,使用-XX:+UseSeria1GC 引數可以指定年輕代和老年代都使用串行收集器,等價于新生代用Serial GC, 且老年代用Serial old GC
4.3.2 ParNew垃圾收集器:并行回收
ParNew介紹
Par是Parallel的縮寫,New: 只能處理的是新生代
如果說Serial GC是年輕代中的單執行緒垃圾收集器,那么ParNew收集器則是Serial收集器的多執行緒版本,
ParNew收集器除了采用并行回收的方式執行記憶體回收外,兩款垃圾收集器之間幾乎沒有任何區別,ParNew收集器在年輕代中同樣也是采用復制演算法、"Stop-the-World"機制 ,
ParNew收集器運行示意圖

對于新生代,回收次數頻繁,使用并行方式高效,
對于老年代,回收次數少,使用串行方式節省資源,(CPU并 行需要切換線
程,串行可以省去切換執行緒的資源)
ParNew與Serial比較
ParNew收集器運行在多CPU的環境下,由于可以充分利用多CPU、多核心等物理硬體資源優勢,可以更快速地完成垃圾收集,提升程式的吞吐量,
但是在單個CPU的環境下,ParNew收集 器不比Serial收集器更高效,雖然Serial收集器是基于串行回收,但是由于CPU不需要頻繁地做任務切換,因此可以有效避免多執行緒互動程序中產生的一些額外開銷,
-XX: +UseParNewGC" 手動指定使用ParNew收集器執行記憶體回收任務只影響年輕代,
-XX:ParallelGCThreads限制執行緒數量,默認開啟和CPU資料相同的執行緒數,
4.3.3 Parallel回收器:吞吐量優先
Paralle介紹
Parallel Scavenge收集器同樣也采用了復制演算法、并行回收和”stop
the World"機制,
和ParNew收集器不同,Parallel Scavenge收集器的目標則是達至一個可控制的吞吐量(Throughput),它也被稱為吞吐量優先的垃圾收集器,
自適應調節策略也是Parallel Scavenge與ParNew一個重要區別,
Parallel收集器運行示意圖

高吞吐量則可以高效率地利用CPU時間,盡快完成程式的運算任務,主要適合在后臺運算而不需要太多互動的任務,因此,常見在服務器環境中使用,例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程式,
Parallel收集器在JDK1.6時提供了用于執行老年代垃圾收集的Parallel old收集器,用來代替老年代的Serial old收集器,
Parallel old收集器采用了標記-壓縮演算法,但同樣也是基于并行回收和"Stop-the-World"機制,
在程式吞吐量優先的應用場景中,Parallel 收集器和Parallel old收集器的組合,在Server模式下的記憶體回收性能很不錯,
在Java8中,默認是此垃圾收集器,
引數配置
-XX:+UseParallel0ldGC手動指定 老年代都是使用并行回收收集器,
分別適用于新生代和老年代,默認jdk8是開啟的,
上面兩個引數,默認開啟一個,另一個也會被開啟, (互相激活)
-XX: ParallelGCThreads設定年輕代并行收集器的執行緒數,
一般最好與CPU數量相等,以避免過多的執行緒數影響垃圾收集性能,
在默認情況下,當CPU數量小于8個,ParallelGCThreads 的值等于CPU數量,
當CPU數量大 于8個,ParallelGCThreads 的值等于
3+ [5*CPU_ Count]/8] ,
-XX:+UseParallelGC
手動指定年輕代使用Parallel并行收集器執行記憶體回收任務,
-XX: GCTimeRatio垃圾收集時間占總時間的比例(= 1 / (N + 1)),
用于衡量吞吐量的大小,
取值范圍(0, 100),默認值99,也就是垃圾回收時間不超過1%,
與前一個-XX:MaxGCPauseMillis引數有一定矛盾性,暫停時間越長,
Radio引數就容易超過設定的比例,
-XX: +UseAdaptiveSizePolilcy
設 置Parallel Scavenge收集器具有自適應調節策略
在這種模式下,年輕代的大小、Eden和Survivor的比例、 晉升老年代的物件年齡
等引數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點,
在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆
目標的吞吐量(GCT imeRatio)和停頓時間(MaxGCPauseMills),
讓虛擬機自己完成調優作業,
-XX:MaxGCPauseMillis設定垃圾收集器最大停頓時間(即STW的時間),單位是毫秒,
為了盡可能地把停頓時間控制在MaxGCPauseMills以內,收集器在,
作業時會調整Java堆大小或者其他一-些引數,
對于用戶來講,停頓時間越短體驗越好,但是在服務器端,我們注重高并發,
整體的吞吐量,所以服務器端適合Parallel,進行控制,該引數使用需謹慎,
4.3.4 CMS回收器:低延遲
CMS介紹
在JDK1.5時期,HotSpot推出了一款在強互動應用中幾乎可認為有劃時代意義的垃圾收集器: CMS (Concurrent -Mark-Sweep )收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的并發收集器,它第一次實作了讓垃圾收集執行緒與用戶執行緒同時作業,
CMS收集器的關注點是盡可能縮短垃圾收集時用戶執行緒的停頓時間,停頓時
間越短( 低延遲)就越適合與用戶互動的程式,良好的回應速度能提升用戶體驗,
目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的回應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗,CMS收集器就非常符合這類應用的需求,
CMS的垃圾收集演算法采用標記-清除演算法,并且也會"Stop- the-world"
CMS收集器作業示意圖

CMS作業原理
CMS整個程序比之前的收集器要復雜,整個程序分為4個主要階段,即初始標記階段、并發標記階段、重新標記階段和并發清除階段,
- 初始標記(Initial-Mark)階段 :在這個階段中,程式中所有的作業執行緒都將會因為“Stop-the-World"機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記出GCRoots能直接關聯到的物件,一旦標記完成之后就會恢復之前被暫停的所有應用執行緒,由于直接關聯物件比較小,所以這里的速度非常快,
- 并發標記(Concurrent-Mark)階段 :從GC Roots的 直接關聯物件開始遍歷整個物件圖的程序,這個程序耗時較長但是不需要停頓用戶執行緒,可以與垃圾收集執行緒一起并發運行,
- 重新標記(Remark) 階段 :由于在并發標記階段中,程式的作業執行緒會和垃圾收集執行緒同時運行或者交叉運行,因此為了修正并發標記期間,因用戶程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比并發標記階段的時間短,
- 并發清除(Concurrent-Sweep)階段 :此階段清理洗掉掉標記階段判斷的已經死亡的物件,釋放記憶體空間,由于不需要移動存活物件,所以這個階段也是可以與用戶執行緒同時并發的
盡管CMS收集器采用的是并發回收(非獨占式),但是在其初始化標記和再次標記這兩個階段中仍然需要執行“Stop-the-World"機制暫停程式中的作業執行緒,不過暫停時間并不會太長,因此可以說明目前所有的垃圾收集器都做不到完全不需要“Stop-the-world”,只是盡可能地縮短暫停時間,
由于最耗費時間的并發標記與并發清除階段都不需要暫停作業,所以整體的回收是低停頓的,
另外,由于在垃圾收集階段用戶執行緒沒有中斷,所以在CMS回收程序中,還應該確保應用程式用戶執行緒有足夠的記憶體可用,因此,CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當堆記憶體使用率達到某一閾值時,便開始進行回收,以確保應用程式在CMS作業程序中依然有足夠的空間支持應用程式運行,要是CMS運行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure失敗,這時虛擬機將啟動后備預案:臨時啟用Serial old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了,

CMS收集器的垃圾收集演算法采用的是標記-清除演算法,這意味著每次執行完內
存回收后,由于被執行記憶體回收的無用物件所占用的記憶體空間極有可能是不連續的一些記憶體塊,不可避免地將會產生一些記憶體碎片,那么CMS在為新物件分配記憶體空間時,將無法使用指標碰撞(Bump the Pointer) 技術,而只能
夠選擇空閑串列(Free List) 執行記憶體分配,
問題分析:既然Mark Sweep 會造成記憶體碎片,那么為什么不把演算法換成
Mark Compact呢?
答案其實很簡答,因為當并發清除的時候,用Compact整理記憶體的話,原
來的用戶執行緒使用的記憶體就不能用了,既然要保證用戶執行緒能繼續執行,前提條件是它的運行的資源不受影響,顯然Mark Compact更適合“Stop the World"這種場景下使用,
CMS優缺點分析
CMS的優點:
- 并發收集
- 低延遲
CMS的缺點:
- 會產生記憶體碎片,導致并發清除后,用戶執行緒可用的空間不足,在無法分配大物件的情況下,不得不提前觸發Full GC,
- CMS收集器對CPU資源非常敏感,在并發階段,它雖然不會導致用戶停頓,但是會因為占用了一部分執行緒而導致應用程式變慢,總吞吐量會降低,
- CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure" 失敗而導致另一次FullGC的產生,在并發標記階段由于程式的作業執行緒和垃圾收集執行緒是同時運行或者交叉運行的,那么在并發標記階段如果產生新的垃圾物件,CMS將 無法對這些垃圾物件進行標記,最侄訓導致這些新產生的垃圾物件沒有被及時回收,從而只能在下一次執行GC時釋放這些之前未被回收的記憶體空間,
浮動垃圾
在CMS的并發標記和并發清理階段,用戶執行緒是還在繼續運行的,程式在運行自然就還會伴隨有新的垃圾物件不斷產生,但這一部分垃圾物件是出現在標記程序結束以后,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉,這一部分垃圾就稱為“浮動垃圾”,
CMS引數設定
-XX:+UseConcMarkSweepGC手動指定使用CMS收集器執行記憶體回收任務,
開啟該引數后會自動將-XX: +UseParNewGC打開,即: ParNew (Young區用) +
CMS (old區用) +Serial old的組合,
-XX:CMSlnitiatingOccupanyFraction設定堆記憶體使用率的閾值,
一旦達到該閾值,便開始進行回收,
JDK5及以前版本的默認值為68,即當老年代的空間使用率達到68%時,會執行
一次CMS回收,JDK6及以上版本默認值為92%
如果記憶體增長緩慢,則可以設定一個稍大的值,大的閾值可以有效降低CMS的觸
發頻率,減少老年代回收的次數可以較為明顯地改善應用程式性能,反之,如
果應用程式記憶體使用率增長很快,則應該降低這個閾值,以避免頻繁觸發老年
代串行收集器,因此通過該選項便可以有效降低Full GC的執行次數,
-XX: +UseCMSCompactAtFullCollection用于指定在執行完Full
GC后對記憶體空間進行壓縮整理,以此避免記憶體碎片的產生,不過由于記憶體
壓縮整理程序無法并發執行,所帶來的問題就是停頓時間變得更長了,
-XX: CMSFullGCsBeforeCompaction設定在執行多少次Full GC后對
記憶體空間進行壓縮整理,
-XX: ParallelCMSThreads設定CMS的執行緒數量,
CMS默認啟動的執行緒數是(ParallelGCThreads+3) /4,
ParallelGCThreads是年輕代并行收集器的執行緒數,當CPU資源比較緊張
時,受到CMs收集器執行緒的影響,應用程式的性能在垃圾回收階段可能會非常糟糕
CMS后續版本中的變化
JDK9新特性: CMS 被標記為Deprecate了
如果對JDK9及以上版本的HotSpot虛擬機使用引數-XX:
+UseConcMarkSweepGC來開啟CMS收集器的話,用戶會收到一個警告資訊,提示CMS未來將會被廢棄,
JDK14新特性:洗掉CMS垃圾回收器
移除了CMS垃圾收集器,如果在JDK14中使用-
XX: +UseConcMarkSweepGC的話,JVM不會報錯,只是給出一個warning資訊,但是不會exit,JVM會自動回退以默認GC方式啟動JVM
4.3.5 G1回收器:區域化分代式
為什么會出現G1?
因為應用程式所應對的業務越來越龐大、復雜,用戶越來越多,沒有GC就不能保證應用程式正常進行,而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化,
G1 (Garbage-First)垃圾回收器是在Java7 update 4之后引入的一個新的垃圾回收器,是當今收集器技術發展的最前沿成果之一,
與此同時,為了適應現在不斷擴大的記憶體和不斷增加的處理器數量,進一步降低暫停時間(pause time) ,同時兼顧良好的吞吐量,
官方給G1設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,所以才擔當起“全功能收集器”的重任與期望,
為什么名字叫做Garbage First (G1)呢?
- 因為G1是一個并行回收器,它把堆記憶體分割為很多不相關的區域(Region) (物理上不連續的),使用不同的Region來表示Eden、幸存者0區,幸存者1區,老年代等,
- G1 GC有計劃地避免在整個Java堆中進行全區域的垃圾收集,G1跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先串列,每次根據允許的收集時間,優先回收價值最大的Region,
- 由于這種方式的側重點在于回收垃圾最大量的區間(Region),所以我們給G1一個名字:垃圾優先(Garbage First) ,
G1的特點
并行與并發
并行性: G1在回收期間,可以有多個GC執行緒同時作業,有效利用多核計算能力,此時用戶執行緒STW
并發性: G1擁有與應用程式交替執行的能力,部分作業可以和應用程式同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程式的情況分代收集
G1是怎么做到如此強大的呢?
G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC),而G1跳出了這個樊籠,它可以面向堆記憶體任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬于哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式,
G1開創的基于Region的堆記憶體布局是它能夠實作這個目標的關鍵,雖然G1也仍是遵循分代收集理論設計的,但其堆記憶體的布局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以
根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間,收集器能夠對扮演不同角色的Region采用不同的策略去處理,這樣無論是新創建的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果,
G1的空間劃分

從上訴劃分中可以看出除了年輕代與老年代的劃分之外,Region中有一類特殊的Humongous區域,該區域專門用來存盤大物件,G1認為只要大小超過了一個Region容量一半的物件即可判定為大物件,每個Region的大小可以通過引數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪,而對于那些超過了整個Region容量的超級大物件,將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待,
磁區Region:化整為零,
使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region
塊,每個Region塊大小根據堆空間的實際大小而定,整體被控制在1MB到32MB之間,且為2的N次冪, 即1MB, 2MB, 4MB, 8MB, 1 6MB, 32MB,可以通過-XX :G1HeapRegionSize設定,所有的Region大小相同,且在JVM生 命周期內不會被改變,
雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region (不需要連續)的集合,通過Region的動態分配方式實作邏輯上的連續,
如果設定了Region數量, 那么Region大小就不是固定的,但是大小肯定是2的冪次方,并且在1-32M之間;如果設定了Region大小,那么Region數量就不是固定的,但是肯定是2048附近;

一個region有可能屬于Eden, Survivor 或者old/Tenured 記憶體區域,但是一
個region只可能屬于一個角色,圖中的E表示該region屬于Eden記憶體區域,s表示屬于Survivor記憶體區域,o表示屬于old記憶體區域,圖中空白的表示未使用的記憶體空間,G1垃圾收集器還增加了一種新的記憶體區域,叫做Humongous 記憶體區域,如圖中的H塊,主要用于存盤大物件,如果超過1. 5個region,就放到H,
設定H的原因:
對于堆中的大物件,默認直接會被分配到老年代,但是如果它是一個短期存在的大物件就會對垃圾收集器造成負面影響,為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放大物件,如果一個H區裝不下一個大物件,那么G1會尋找連續的H區來存盤,為了能找到連續的H區,有時候不得不啟動Full GC, G1的大多數行為都把H區作為老年代的一部分來看待,
G1回收器垃圾回收程序

應用程式分配記憶體,當年輕代的Eden區用盡時開始年輕代回收程序; G1的年輕代收集階段是一個并行的獨占式收集器,在年輕代回收期,G1 GC暫停所有應用程式執行緒,啟動多執行緒執行年輕代回收,然后從年輕代區間移動存活物件到Survivor區間或者老年區間,也有可能是兩個區間都會涉及,
當堆記憶體使用達到一定值(默認45%)時,開始老年代并發標記程序,標記完成馬上開始混合回收程序,對于一個混合回收期,G1 GC從老年區間移動存活物件到空閑區間,這些空閑區間也就成為了老年代的一部分,和年輕代不同,老年代的G1回收器和其他GC不同,G1的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收一小部分老年代的Region就可以了,同時,這個老年代Region是和年輕代一起被回收的,
舉個例子: 一個web服務器,Java行程最大堆記憶體為4G,每分鐘回應1500個請求,每45秒鐘會新分配大約2G的記憶體,G1會每45秒鐘進行一次年輕代回收,每31個小時整個堆的使用率會達到45號,會開始老年代并發標記程序,標記完成后開始四到五次的混合回收,
G1作業示意圖

G1 GC的垃圾回收程序的具體階段分析:
1. 年輕代GC (Young GC )
具體程序如下:
JVM啟動時,G1先準備好Eden區,程式在運行程序中不斷創建物件到Eden
區,當Eden空間耗盡時,G1會啟動一次年輕代垃圾回收程序,年輕代垃圾回收只會回收Eden區和Survivor區,
首先:G1停止應用程式的執行(Stop-The-World) ,G1創建回收集
(Collection Set) ,回收集是指需要被回收的記憶體分段的集合,年輕代回收程序的回收集包含年輕代Eden區和Survivor區所有的記憶體分段,
然后開始如下回收程序:
第一階段,掃描根,可以體現Rset作用:避免全堆掃描
根是指static變數指向的物件,正在執行的方法呼叫鏈條上的區域變數等,根參考連同RSet記錄的外部參考作為掃描存活物件的入口,
第二階段,更新RSet,作用:保證Rset中的資料準確性
處理dirty card queue中的card,更新RSet,此階段完成后,RSet可以準確的反映老年代對所在的記憶體分段中物件的參考,
dirty card queue(臟卡表佇列)解釋
對于應用程式的參考賦值陳述句object.field=object, JVM會在之前和之后執行特殊的操作以在dirty card queue中入隊一個保存了物件參考資訊的card,在年輕代回收的時候,G1會對Dirty Card Queue中所有的card進行處理,以更新RSet,保證RSet實時準確的反映參考關系,因為RSet的處理需要執行緒同步,開銷會很大,使用佇列性能會好很多,
其中objec.field=object中的第一個object代表老年代中的物件,而第二個object代表Eden區中的物件
臟卡表佇列作用:
Reset更新需要執行緒同步,所以開銷會很大,因此不能實時更新,因此我們需要把參考物件被其他物件參考的關系放在一個臟卡表佇列中,當年輕代回收的時候會進行STW,所以我們也正好把臟卡表佇列中的值更新到Rset中,這樣不僅沒有涉及到開銷問題,還可以保證Rset中的資料是準確的
第三階段,處理RSet,作用: 根可達性遍歷的一部分
識別被老年代物件指向的Eden中的物件,這些被指向的Eden中的物件被認為是存活的物件,
第四階段,復制物件,說明: 新生代使用復制演算法
此階段,物件樹被遍歷,Eden區記憶體段中存活的物件會被復制到Survivor區中空的記憶體分段,Survivor區記憶體段中存活的物件如果年齡未達閾值,年齡會加1,達到閥值會被會被復制到old區中空的記憶體分段,如果Survivor空間不夠, Eden 空間的部分資料會直接晉升到老年代空間,
第五階段,處理參考,
空Eden: Eden變成空的,那它就變成了無主Region,因此會被記錄到空鏈表中,等待下一次被分配處理Soft,Weak, Phantom, Final, JNI Weak 等參考,最終Eden空間的資料為空,GC停止作業,而目標記憶體中的物件都是連續存盤的,沒有碎片,所以復制程序可以達到記憶體整理的效果,減少碎片,以上回收的都是強參考物件,下面回收軟參考物件(不足回收)、弱參考物件(發現回收)、虛參考物件,
2. 老年代并發標記程序( Concurrent Marking )
- 初始標記階段:標記從根節點直接可達的物件,這個階段是STW的,并且會觸發一次年輕代GC,
- 根區域掃描(Root Region Scanning) : G1 GC掃描Survivor區直接可達的老年代區域物件,并標記被參考的物件,這一程序必須在youngGC之前完成,主要掃描哪些老年代物件是可達的畢竟我們進行young GC的時候會移動Survivor區,移動之后就找不到哪些老年代物件是可達的了
- 并發標記(Concurrent Marking): 在整個堆中進行并發標記(和應用程式并發執行),此程序可能被youngGC中斷,在并發標記階段,若發現區域物件中的所有物件都是垃圾,那這個區域會被立即回收,同時,并發標記程序中,會計算每個區域的物件活性(區域中存活物件的比例),
- 再次標記(Remark):由 于應用程式持續進行,需要修正上一次的標記結果,是STW的,G1中采用了比CMS更快的初始快照演算法: snapshot-at-the-beginning (SATB), 原因:并發標記不準確
- 獨占清理(cleanup ,STW): 計算各個區域的存活物件和GC回收比例,并進行排序,識別可以混合回收的區域,為下階段做鋪墊,是STW的, 其實是一個統計計算程序,不會涉及垃圾清理,這個階段并不會實際上去做垃圾的收集
- 并發清理階段:識別并清理完全空閑的區域,
并發清理階段任務:如果發現區域物件中的所有物件都是垃圾,那么這個區域會被立即回收
3. 混合回收(Mixed GC )
當越來越多的物件晉升到老年代old region時,為了避免堆記憶體被耗盡,虛擬機會觸發一個混合的垃圾收集器,即Mixed GC,該演算法并不是一個old GC,除了回收整個Young Region,還會回收一部分的oldRegion,
注意:是一部分老年代,而不是全部老年代,是Mixed GC并不是Full GC,
4. 如果需要,單執行緒、獨占式、高強度的Full GC還是繼續存在的,它針對GC的評估失敗提供了一種失敗保護機制,即強力回收,
導致G1Full GC的原因:
- Evacuation的時候沒有足夠的to-space來存放晉升的物件; 解決辦法:加大堆空間
- 并發處理程序完成之前空間耗盡,解決辦法:調小觸發并發GC周期的ava堆占用閾值(默認是45%)
- 最大GC停頓時間太短,導致在規定的時間間隔內無法完成垃圾回收,也會導致Full GC 解決辦法:加大最大GC停頓時間
G1收集程序遇到的問題
- 一個物件被不同區域參考的問題:一個Region不可能是孤立的,一個Region中的物件可能被其他任意Region中物件參考,
- 判斷物件存活時,是否需要掃描整個Java堆才能保證準確?
- 回收新生代也不得不同時掃描老年代?這樣的話會降低Minor GC的效率;
解決方法:
無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全域掃描:
- 每個Region都有一個對應的Remembered Set;
- 每次Reference型別資料寫操作時,都會產生一個Write Barrier暫時中斷操作;
- 然后檢查將要寫入的參考指向的物件是否和該Reference型別資料在不同的Region (其他收集器:檢查老年代物件是否參考了新生代物件) ;
- 如果不同,通過CardTable把相關參考資訊記錄到參考指向物件的所在Region對應的Remembered Set中;
- 當進行垃圾收集時,在GC根節點的列舉范圍加入Remembered Set; 就可以保證不進行全域掃描,也不會有遺漏,
Remembered Set

Remebered Set就是圖中的Reset,Reference型別就是參考型別,其中Reset的作用是記錄當前Region中哪些物件被外部參考指向,比如old區中的物件會指向Eden區的物件,然后當我們要回收某個Region的時候,直接遍歷遍歷當前Region中的所有物件就可以了,然后針對性的去找到那些指向當前物件的其他物件,最終發現當前物件是否是根可達的,如果不是,那就應該被洗掉,其實之前的垃圾回收器都涉及到這個問題,當進行Minor GC的時候,通過GC Roots查找的時候還需要遍歷old區的物件,畢竟oId區物件也可能會指向Eden區物件,但是G1通過Rset避免了全堆的掃描,當參考型別資料寫操作時,先暫時中斷,然后判斷當前參考型別資料是否被其他物件所指向,如果不被指向,那就直接放在Region中就可以了;如果被其他物件指向,那么還要判斷這個物件是在當前要插入的Region中,還是在其他Region中;如果在其他Region中,那就需要使用CardTable把當前參考型別資料的指向資訊放在Rset中,也就是形成上面的虛線連線,如果在當前Region中,那就不需要指向了,畢竟到時候我們會進行遍歷查找根可達物件,那肯定會找到的,所以這種情況也是直接放在Region中就可以了;
G1的優勢
1)空間整合
G1將記憶體劃分為一個個的region, 記憶體的回收是以region作為基本單位的,Region之間是復制演算法,但整體上實際可看作是標記-壓縮(Mark-Compact) 演算法,兩種演算法都可以避免記憶體碎片,這種特性有利于程式長時間運行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC,尤其是當Java堆非常大的時候,G1的優勢更加明顯,
2)可預測的停頓時間模型(即:軟實時soft real-time)
這是G1相對于CMS的另一大優勢,G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內消耗在垃圾收集上的時間不得超過N毫秒,
- 由于磁區的原因,G1可以只選取部磁區域進行記憶體回收,這樣縮小了回收的范圍,因此對于全域停頓情況的發生也能得到較好的控制,
- G1跟蹤各個Region 里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先串列,每次根據允許的收集時間,優先回收價值最大的Region,保證了G1收集器在有限的時間內可以獲取盡可能高的收集效率,
- 相比于CMSGC,G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多,
G1相較于CMS的缺點
相較于CMS,G1還不具備全方位、壓倒性優勢,比如在用戶程式運行程序中,G1無論是為了垃圾收集產生的記憶體占用(Footprint) 還是程式運行時的額
外執行負載(Overload)都要比CMS要高,
從經驗上來說,在小記憶體應用上.CMS的表現大概率會優于G1,而G1在大記憶體應用上則發揮其優勢,平衡點在6-8GB之間,
G1的常見引數設定
- XX: +UseG1GC
手動指定使用G1收集器執行記憶體回收任務,
- XX:G1HeapRegionSize設 置每個Region的大小,值是2的冪,范圍是1MB
到32MB之間,目標是根據最小的Java堆大小劃分出約2048個區域,
默認是堆記憶體的1/2000,
- XX: MaxGCPauseMillis
設定期望達到的最大GC停頓時間指標(JVM會盡力實作,但不保證達到),
默認值是200ms
-XX: ParallelGCThread
設定STW時GC執行緒數的值,最多設定為8
- XX: ConcGCThreads
設定并發標記的執行緒數,
將n設定為并行垃圾回收執行緒數(ParallelGCThreads)的1/4左右,
-XX: InitiatingHeapoccupancyPercent
設定觸發并發GC周期的Java堆占用率閾值,超過此值,就觸發GC,
默認值是45,堆空間已用占比達到45%,老年代才會并發標記
G1回收器的常見操作步驟
G1的設計原則就是簡化JVM性能調優,開發人員只需要簡單的三步即可完成
調優:
第一步:開啟G1垃圾收集器
第二步:設定堆的最大記憶體
第三步:設定最大的停頓時間
G1中提供了三種垃圾回收模式: YoungGC、 Mixed GC和Full GC,在不同的條件下被觸發,
G1回收器的適用場景
-
面向服務端應用,針對具有大記憶體、多處理器的機器,(在普通大小的堆里表現并不驚喜)
-
最主要的應用是需要低GC延遲,并具有大堆的應用程式提供解決方案;
如:在堆大小約6GB或更大時,可預測的暫停時間可以低于0.5秒; (G1通過每次只清理一部分而不是全部的Region的增量式清理來保證每次GC停頓時間不會過長), -
用來替換掉JDK1.5中的CMS收集器;
在下面的情況時,使用G1可能比CMS好:
①超過50%的Java堆被活動資料占用;
②物件分配頻率或年代提升頻率變化很大;
③GC停頓時間過長(長于0.5至1秒), -
HotSpot垃圾收集器里,除了G1以外,其他的垃圾收集器使用內置的JVM執行緒執行GC的多執行緒操作,而G1 GC可以采用應用執行緒承擔后臺運行的GC作業,即當JVM的GC執行緒處理速度慢時,系統會呼叫應用程式執行緒幫助加速垃圾回收程序,
4.4 七款經典垃圾收集器總結
| 垃圾收集器 | 分類 | 作用位置 | 使用演算法 | 特點 | 適用場景 |
|---|---|---|---|---|---|
| Serial | 串行運行 | 作用于新生代 | 復制演算法 | 回應速度優先 | 適用于單CPU環境下的client模式 |
| ParNew | 并行運行 | 作用于新生代 | 復制演算法 | 回應速度優先 | 多CPU環境Server模式下與CMS配合使用 |
| Parallel | 并行運行 | 作用于新生代 | 復制演算法 | 吞吐量優先 | 適用于后臺運算而不需要太多互動的場景 |
| Serial Old | 串行運行 | 作用于老年代 | 標記-壓縮演算法 | 回應速度優先 | 適用于單CPU環境下的Client模式 |
| Parallel Old | 并行運行 | 作用于老年代 | 標記-壓縮演算法 | 吞吐量優先 | 適用于后臺運算而不需要太多互動的場景 |
| CMS | 并發運行 | 作用于老年代 | 標記清除演算法 | 回應速度優先 | 適用于互聯網或B/S業務 |
| G1 | 并發、并行運行 | 作用于老年代和新生代 | 標記-壓縮演算法、復制演算法 | 回應速度優先 | 面向服務端應用 |
怎么選擇垃圾收集器?
- 優先調整堆的大小讓JVM自適應完成,
- 如果記憶體小于100M,使用串行收集器
- 如果是單核、單機程式,并且沒有停頓時間的要求,串行收集器
- 如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇并行或者JVM自己選擇
- 如果是多CPU、追求低停頓時間,需快速回應(比如延遲不能超過1秒,如互聯網應用),使用并發收集器,
官方推薦G1,性能高,現在互聯網的專案,基本都是使用G1,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/292703.html
標籤:java
上一篇:Flume【環境搭建 01】【apache-flume-1.9.0 安裝配置】【Linux環境 騰訊云 CentOS Linux release 7.5.1804】
