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