前言
在之前的幾篇部落格中,我們大致介紹了,常見的 垃圾回收演算法 及 JVM
中常見的分類回收演算法。這些都是從演算法和規範上分析 Java
中的垃圾回收,屬於方法論。在 JVM
中,垃圾回收的具體實現是由 垃圾回收器(Garbage Collector
)負責的。
正文
概述
在瞭解 垃圾回收器 之前,首先得了解一下垃圾回收器的幾個名詞。
1. 吞吐量
CPU
用於執行使用者程式碼的時間與 CPU
總消耗時間的比值。比如說虛擬機器總執行了 100
分鐘,使用者程式碼 時間 99
分鐘,垃圾回收 時間 1
分鐘,那麼吞吐量就是 99%
。
吞吐量 = 執行使用者程式碼時間/(執行使用者程式碼時間 + 垃圾回收時間)
2. 停頓時間
停頓時間 指垃圾回收器正在執行時,應用程式 的 暫停時間。對於 獨佔回收器 而言,停頓時間可能會比較長。使用 併發回收器 時,由於垃圾回收器和應用程式 交替執行,程式的 停頓時間 會變短,但是,由於其 效率 很可能不如獨佔垃圾回收器,故系統的 吞吐量 可能會較低。
3. GC的名詞
3.1. 新生代GC(Minor GC)
指發生在 新生代 的垃圾回收動作,因為 Java
物件大多都具備 朝生夕死 的特性,所以 Minor GC
通常 非常頻繁,一般回收速度也比較快。
3.2. 老年代GC(Major GC)
指發生在 老年代 的垃圾回收動作,出現了 Major GC
,經常會伴隨至少一次的 Minor GC
(發生這種情況,那麼 整個堆 都 GC
一遍,通常稱為 Full GC
)。Major GC
的速度一般會比 Minor GC
慢 10
倍以上。
4. 併發與並行
4.1. 序列(Parallel)
單執行緒 進行垃圾回收工作,但此時 使用者執行緒 仍然處於 等待狀態。
4.2. 併發(Concurrent)
這裡的併發指 使用者執行緒 與 垃圾回收執行緒 交替執行。
4.3. 並行(Parallel)
這裡的並行指 使用者執行緒 和多條 垃圾回收執行緒 分別在不同 CPU
上同時工作。
垃圾回收演算法
1. 根搜尋演算法
根搜尋演算法 是從 離散數學 中的圖論引入的,程式把所有引用關係看作一張圖,從一個節點 GC ROOT
開始,尋找對應的 引用節點,找到這個節點後,繼續尋找 這個節點 的 引用節點。當所有的引用節點尋找完畢後,剩餘的節點 則被認為是 沒有被引用到 的節點,即 無用 的節點。
上圖 紅色 為無用的節點,可以被 回收。目前 Java
中可以作為 GC ROOT
的物件有:
-
虛擬機器棧 中引用的物件(本地變數表);
-
方法區 中 靜態變數 引用的物件;
-
方法區 中 常量 引用的物件;
-
本地方法棧 中引用的物件(
Native
物件)。
基本所有
GC
演算法都引用 根搜尋演算法 這種概念。
2. 標記 – 清除演算法
標記-清除演算法 從 根集合 進行掃描,對 存活的物件 進行 標記。標記完畢後,再掃描整個空間中 未被標記 的物件進行 直接回收,如下圖所示:
標記-清除演算法 不需要進行 物件的移動,並且僅對 不存活 的物件進行處理,在 存活 的物件 比較多 的情況下 極為高效。但由於 標記-清除演算法 直接回收不存活的物件,並沒有對還存活的物件進行 整理,因此會導致 記憶體碎片。
3. 複製演算法
複製演算法 將記憶體劃分為 兩個區間,使用此演算法時,所有 動態分配 的物件都只能分配在 其中一個 區間(活動區間),而 另外一個 區間(空間區間)則是 空閒 的。
複製演算法 同樣從 根集合 掃描,將 存活 的物件 複製 到 空閒區間。當掃描完畢活動區間後,會的將 活動區間 一次性全部 回收。此時原本的 空閒區間 變成了 活動區間。下次 GC
時候又會重複剛才的操作,以此迴圈。
複製演算法 在存活物件 比較少 的時候,極為高效,但是帶來的成本是 犧牲一半的記憶體空間 用於進行 物件的移動。所以 複製演算法 的使用場景,必須是物件的 存活率非常低 才行。最重要的是,我們需要克服 50%
的 記憶體浪費。
4. 標記 – 整理演算法
標記-整理演算法 採用 標記-清除演算法 一樣的方式進行物件的 標記,但在回收 不存活的物件 佔用的空間後,會將所有 存活的物件 往 左端空閒空間 移動,並更新對應的指標。
標記-整理 是在 標記-清除 之上,又進行了 物件的移動排序整理,因此 成本更高,但卻解決了 記憶體碎片 的問題。
JVM
為了 優化記憶體 的回收,使用了 分代回收 的方式。對於 新生代記憶體 的回收(Minor GC
)主要採用 複製演算法。而對於 老年代記憶體 的回收(Major GC
),大多采用 標記-整理演算法。
垃圾回收器
1. 垃圾回收器分類標準
2. 七種垃圾回收器概述
在 JVM
中,具體實現有 Serial
、ParNew
、Parallel Scavenge
、CMS
、Serial Old(MSC)
、Parallel Old
、G1
等。在下圖中,你可以看到 不同垃圾回收器 適合於 不同的記憶體區域,如果兩個垃圾回收器之間 存在連線,那麼表示兩者可以 配合使用。
如果當 垃圾回收器 進行垃圾清理時,必須 暫停 其他所有的 工作執行緒,直到它完全收集結束。我們稱這種需要暫停工作執行緒才能進行清理的策略為 Stop-the-World
。以上回收器中, Serial
、ParNew
、Parallel Scavenge
、Serial Old
、Parallel Old
均採用的是 Stop-the-World
的策略。
圖中有 7
種不同的 垃圾回收器,它們分別用於不同分代的垃圾回收。
-
新生代回收器:Serial、ParNew、Parallel Scavenge
-
老年代回收器:Serial Old、Parallel Old、CMS
-
整堆回收器:G1
兩個 垃圾回收器 之間有連線表示它們可以 搭配使用,可選的搭配方案如下:
新生代 | 老年代 |
---|---|
Serial | Serial Old |
Serial | CMS |
ParNew | Serial Old |
ParNew | CMS |
Parallel Scavenge | Serial Old |
Parallel Scavenge | Parallel Old |
G1 | G1 |
3. 單執行緒垃圾回收器
3.1. Serial(-XX:+UseSerialGC)
Serial
回收器是最基本的 新生代 垃圾回收器,是 單執行緒 的垃圾回收器。由於垃圾清理時,Serial
回收器 不存在 執行緒間的切換,因此,特別是在單 CPU
的環境下,它的 垃圾清除效率 比較高。對於 Client
執行模式的程式,選擇 Serial
回收器是一個不錯的選擇。
Serial
新生代回收器 採用的是 複製演算法。
3.2. Serial Old(-XX:+UseSerialGC)
Serial Old
回收器是 Serial
回收器的 老生代版本,屬於 單執行緒回收器,它使用 標記-整理 演算法。對於 Server
模式下的虛擬機器,在 JDK1.5
及其以前,它常與 Parallel Scavenge
回收器配合使用,達到較好的 吞吐量,另外它也是 CMS
回收器在 Concurrent Mode Failure
時的 後備方案。
Serial
回收器和 Serial Old
回收器的執行效果如下:
Serial Old
老年代回收器 採用的是 標記 – 整理演算法。
4. 多執行緒垃圾回收器(吞吐量優先)
4.1. ParNew(-XX:+UseParNewGC)
ParNew
回收器是在 Serial
回收器的基礎上演化而來的,屬於 Serial
回收器的 多執行緒版本,同樣執行在 新生代區域。在實現上,兩者共用很多程式碼。在不同執行環境下,根據 CPU
核數,開啟 不同的執行緒數,從而達到 最優 的垃圾回收效果。對於那些 Server
模式的應用程式,如果考慮採用 CMS
作為 老生代回收器 時,ParNew
回收器是一個不錯的選擇。
ParNew
新生代回收器 採用的是 複製演算法。
4.2. Parallel Scavenge(-XX:+UseParallelGC)
和 ParNew
回收一樣,Parallel Scavenge
回收器也是執行在 新生代區域,屬於 多執行緒 的回收器。但不同的是,ParNew
回收器是通過控制 垃圾回收 的 執行緒數 來進行引數調整,而 Parallel Scavenge
回收器更關心的是 程式執行的吞吐量。即一段時間內,使用者程式碼 執行時間佔 總執行時間 的百分比。
Parallel Scavenge
新生代回收器 採用的是 複製演算法。
4.3. Parallel Old(-XX:+UseParallelOldGC)
Parallel Old
回收器是 Parallel Scavenge
回收器的 老生代版本,屬於 多執行緒回收器,採用 標記-整理演算法。Parallel Old
回收器和 Parallel Scavenge
回收器同樣考慮了 吞吐量優先 這一指標,非常適合那些 注重吞吐量 和 CPU
資源敏感 的場合。
Parallel Old
老年代回收器 採用的是 標記 – 整理演算法。
5. 其他的回收器(停頓時間優先)
5.1. CMS(-XX:+UseConcMarkSweepGC)
CMS(Concurrent Mark Sweep)
回收器是在 最短回收停頓時間 為前提的回收器,屬於 多執行緒回收器,採用 標記-清除演算法。
相比之前的回收器,CMS
回收器的運作過程比較複雜,分為四步:
- 初始標記(CMS initial mark)
初始標記 僅僅是標記 GC Roots
內 直接關聯 的物件。這個階段 速度很快,需要 Stop the World
。
- 併發標記(CMS concurrent mark)
併發標記 進行的是 GC Tracing
,從 GC Roots
開始對堆進行 可達性分析,找出 存活物件。
- 重新標記(CMS remark)
重新標記 階段為了 修正 併發期間由於 使用者進行運作 導致的 標記變動 的那一部分物件的 標記記錄。這個階段的 停頓時間 一般會比 初始標記階段 稍長一些,但遠比 併發標記 的時間短,也需要 Stop The World
。
- 併發清除(CMS concurrent sweep)
併發清除 階段會清除垃圾物件。
初始標記(
CMS initial mark
)和 重新標記(CMS remark
)會導致 使用者執行緒 卡頓,Stop the World
現象發生。
在整個過程中,CMS
回收器的 記憶體回收 基本上和 使用者執行緒 併發執行,如下所示:
由於 CMS
回收器 併發收集、停頓低,因此有些地方成為 併發低停頓回收器(Concurrent Low Pause Sweep Collector
)。
CMS
回收器的缺點:
- CMS回收器對CPU資源非常依賴
CMS
回收器過分依賴於 多執行緒環境,預設情況下,開啟的 執行緒數 為(CPU 的數量 + 3)/ 4
,當 CPU
數量少於 4
個時,CMS
對 使用者查詢 的影響將會很大,因為他們要分出一半的運算能力去 執行回收器執行緒;
- CMS回收器無法清除浮動垃圾
由於 CMS
回收器 清除已標記的垃圾 (處於最後一個階段)時,使用者執行緒 還在執行,因此會有新的垃圾產生。但是這部分垃圾 未被標記,在下一次 GC
才能清除,因此被成為 浮動垃圾。
由於 記憶體回收 和 使用者執行緒 是同時進行的,記憶體在被 回收 的同時,也在被 分配。當 老生代 中的記憶體使用超過一定的比例時,系統將會進行 垃圾回收;當 剩餘記憶體 不能滿足程式執行要求時,系統將會出現 Concurrent Mode Failure
,臨時採用 Serial Old
演算法進行 清除,此時的 效能 將會降低。
- 垃圾收集結束後殘餘大量空間碎片
CMS
回收器採用的 標記清除演算法,本身存在垃圾收集結束後殘餘 大量空間碎片 的缺點。CMS
配合適當的 記憶體整理策略,在一定程度上可以解決這個問題。
5.2. G1回收器(垃圾區域Region優先)
G1
是 JDK 1.7
中正式投入使用的用於取代 CMS
的 壓縮回收器。它雖然沒有在物理上隔斷 新生代 與 老生代,但是仍然屬於 分代垃圾回收器。G1
仍然會區分 年輕代 與 老年代,年輕代依然分有 Eden
區與 Survivor
區。
G1
首先將 堆 分為 大小相等 的 Region
,避免 全區域 的垃圾回收。然後追蹤每個 Region
垃圾 堆積的價值大小,在後臺維護一個 優先列表,根據允許的回收時間優先回收價值最大的 Region
。同時 G1
採用 Remembered Set
來存放 Region
之間的 物件引用 ,其他回收器中的 新生代 與 老年代 之間的物件引用,從而避免 全堆掃描。G1
的分割槽示例如下圖所示:
這種使用 Region
劃分 記憶體空間 以及有 優先順序 的區域回收方式,保證 G1
回收器在有限的時間內可以獲得儘可能 高的回收效率。
G1
和 CMS
運作過程有很多相似之處,整個過程也分為 4
個步驟:
- 初始標記(CMS initial mark)
初始標記 僅僅是標記 GC Roots
內 直接關聯 的物件。這個階段 速度很快,需要 Stop the World
。
- 併發標記(CMS concurrent mark)
併發標記 進行的是 GC Tracing
,從 GC Roots
開始對堆進行 可達性分析,找出 存活物件。
- 重新標記(CMS remark)
重新標記 階段為了 修正 併發期間由於 使用者進行運作 導致的 標記變動 的那一部分物件的 標記記錄。這個階段的 停頓時間 一般會比 初始標記階段 稍長一些,但遠比 併發標記 的時間短,也需要 Stop The World
。
- 篩選回收
首先對各個 Region
的 回收價值 和 成本 進行排序,根據使用者所期望的 GC
停頓時間 來制定回收計劃。這個階段可以與使用者程式一起 併發執行,但是因為只回收一部分 Region
,時間是使用者可控制的,而且停頓 使用者執行緒 將大幅提高回收效率。
與其它
GC
回收相比,G1
具備如下4
個特點:
- 並行與併發
使用多個 CPU
來縮短 Stop-the-World
的 停頓時間,部分其他回收器需要停頓 Java
執行緒執行的 GC
動作,G1
回收器仍然可以通過 併發的方式 讓 Java
程式繼續執行。
- 分代回收
與其他回收器一樣,分代概念 在 G1
中依然得以保留。雖然 G1
可以不需要 其他回收器配合 就能獨立管理 整個GC堆,但它能夠採用 不同的策略 去處理 新建立的物件 和 已經存活 一段時間、熬過多次 GC
的舊物件,以獲取更好的回收效果。新生代 和 老年代 不再是 物理隔離,是多個 大小相等 的獨立 Region
。
- 空間整合
與 CMS
的 標記—清理 演算法不同,G1
從 整體 來看是基於 標記—整理 演算法實現的回收器。從 區域性(兩個 Region
之間)上來看是基於 複製演算法 實現的。
但無論如何,這 兩種演算法 都意味著 G1
運作期間 不會產生記憶體空間碎片,回收後能提供規整的可用記憶體。這種特性有利於程式長時間執行,分配大物件 時不會因為無法找到 連續記憶體空間 而提前觸發 下一次 GC
。
- 可預測的停頓
這是 G1
相對於 CMS
的另一大優勢,降低停頓時間 是 G1
和 CMS
共同的關注點。G1
除了追求 低停頓 外,還能建立 可預測 的 停頓時間模型,能讓使用者明確指定在一個 長度 為 M
毫秒的 時間片段 內,消耗在 垃圾回收 上的時間不得超過 N
毫秒。(後臺維護的 優先列表,優先回收 價值大 的 Region
)。
參考
周志明,深入理解Java虛擬機器:JVM高階特性與最佳實踐,機械工業出版社
歡迎關注技術公眾號:零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。