程式扎記: [ JVM 應用 ] 垃圾收集器與記憶體分配策略 - Part2

標籤

2012年6月30日 星期六

[ JVM 應用 ] 垃圾收集器與記憶體分配策略 - Part2

垃圾收集演算法 : 
在 Part1 我們介紹了 JVM 如何確認物件存活使用的演算法, 當確定某物件不再被參考到或存活, 接著便是要把它們當作垃圾收集摟. 這邊介紹幾種常見的垃圾收集演算法. 

- 標記/清除 演算法 
最基礎的收集演算法便是 "標記-清除" (Mark-Sweep) 演算法, 如它的名字一樣, 演算法分為 "標記" 和 "清除" 兩個階段 : 第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。此演算法需要暫停整個應用,同時,會產生記憶體碎片. 示意圖如下 : 
 

- 複製演算法 
此 演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域. 垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中. 演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不過出現 "碎片" 問題. 當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間 : 
 

- 標記/整理 演算法 
複製演算法在物件存活率較高的場合需要進行較多的複製操作, 效率將打折扣. 更關鍵的是如果不想浪費 50% 空間, 就需要有額外空間進行分配擔保, 以應對使用的記憶體所有物件都 100% 存活的極端狀況, 所以在老年代一般不會採用這種演算法. 而 "標記-整理" 演算法結合了 "標記-清除" 和 "複製" 兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個 Heap,清除未標記物件並且把存活物件 "壓縮" 到堆的其中一塊並按順序排放。此演算法避免了"標記-清除" 的碎片問題,同時也避免了 "複製" 演算法的空間問題. 

- 分代演算法(Generational Collecting) 
基於對物件生命週期分析後得出的垃圾回收演算法。把物件分為年青代(Young)、年老代(Tenured) 與 持久代(Perm),對不同生命週期的物件使用不同的演算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此演算法的。 

垃圾收集器 : 
如果說收集演算法是記憶體回收的方法論, 垃圾收集器就是記憶體回收的具體實現. JVM 規範中對 垃圾收集器 如何實現並沒有任何規定, 因此不同廠商, 不同版本的 JVM 的 垃圾收集器可能會有不小的差別, 並且一般都會提供參數提供使用者根據自己的應用特點與要求組合出各個年代所使用的收集器. 底下是 Sun HotSpot JVM 1.6 update 22 所包含的所有收集器 :  

- Serial 收集器/串列收集器 
使用單執行緒所有垃圾回收工作,因為無需多執行緒交互,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單一處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。可以使用 -XX:+UseSerialGC 打開. 
 

- ParNew 收集器 
ParNew 收集器其實就是 Serial 收集器的多執行序版本, 除了使用多執行序進行垃圾收集外, 你也可以透過參數 -XX:ParallelGCThreads 控制其開啟的線程數. ParNew 收集器在單 CPU 環境絕對不會有比 Serial 收集器更好的效能, 由於其在執行序間互動的開銷, 甚至在兩個 CPU 都不能保證超越 Serial 收集器. 當然隨著 CPU 數量增加, 自然效能會越來越好. 
 

- Parallel Scavenge 收集器 
Parallel Scavenge 是一個新生代收集器, 使用複製演算法也是平行的多執行緒收集器 (與 ParNew 沒兩樣?). 它的特點是它提供參數讓你可以透過參數設定控制 "傳輸量" 的能力. 因此你可以決定進行 GC 時的停頓時間. 所謂 "傳輸量" (Throughput) 即是 : 
傳輸量 = 執行用戶程式碼時間 / (執行用戶程式碼時間 + 垃圾收集時間)

考慮 JVM 共執行 100 分鐘, 其中 GC 回收花掉 1 分鐘, 那傳輸量就是 99%. 停頓時間越短就越適合需要與使用者互動的程式, 而高傳輸量可以高效率地利用 CPU 時間完成運算任務而減少使用者等待時間. Parallel Scavenge 收集器提供兩個參數用於精準控制傳輸量, 分別是控制最大 GC 停頓時間的 -XX:MaxGCPauseMillis (>0) 與直接設置傳輸量大小的 -XX:GCTimeRatio 參數 (>0 && <100). 除了上述兩個參數外, Parallel Scavenge 還有一個參數 -XX:+UseAdaptiveSizePolicy 值得關注. 這是一個開關參數, 當打開後就不需要手動指定新生代的大小 (-Xmn) , Eden 與 Survivor 區的比例 -XX:SurvivorRatio 等細節參數, JVM 會自動根據當時系統地執行狀態, 收集性能資訊並動態的自我調整 (GC Ergonomics). 

- Serial Old 收集器 
Serial Old 是 Serial 收集器的老年代版本, 它同樣也是一個單執行序收集器. 使用 "標記-整理" 演算法. 

- Parallel Old 收集器 
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多執行序和 "標記-整理" 演算法. 這個收集器是在 JDK 1.6 中才開始提供. 在注重傳輸量與 CPU 資源敏感的場合, 都可以優先考慮使用 Parallel Scavenge + Parallel Old 收集器組合. 
 

- CMS 收集器 
CMS (Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間為目標的收集器. 目前很大一部分的 Java 應用都集中在網際網路或 B/S 系統的服務端上, 這類應用尤其重視服務的回應速度, 希望系統停頓時間越短以給用戶帶來較好的體驗. CMS 收集器非常符合這類應用需求. 

從名字就可以知道 CMS 收集器是基於 "標記-清除" 演算法實現的, 它的運作過程相對於前面幾種收集器要複雜一些, 整個過程分為 4 個步驟 : 
* 初始標記 (CMS initial mark)
* 並行標記 (CMS concurrent mark)
* 重新標記 (CMS remark)
* 並行清除 (CMS concurrent sweep)

其中初始標記, 重新標記這兩個步驟仍然需要 "Stop The World" (將使用者線程暫停). 初始標記僅僅是標記一下 GC Roots 能直接關連到的物件, 速度很快 ; 並行標記階段就是進行 GC Roots Tracing 的過程 ; 而重新標記則是為了修正並行標記期間, 因使用者程式繼續運作導致標記產生變動那一部分物件的標記紀錄, 這個階段的停頓時間一般會比初始標記稍長一些, 但遠比並行標記的時間短. 

由於整個過程中耗時最長的並行標記與並行清除中, 收集器執行序都可以與用戶執行序一起工作, 所以整體來說 CMS 收集器的記憶體回收過程是與使用者執行序一起並行的執行 : 
 

CMS是一款優秀的收集器 : 並行收集, 低停頓. 但是 CMS 還遠不到完美的程度, 它有以下三個顯著缺點 : 
* CMS 收集器對 CPU 資源非常敏感 
其實面向並行設計的程式都對 CPU 資源比較敏感. 在並行階段, 它雖然不會導致用戶執行緒停頓, 但是會因為占用部分執行緒 (或者說 CPU 資源) 而導致應用程式變慢, 總傳輸量會降低. CMS 預設啟動的回收執行緒是 (CPU 數量 + 3)/4, 也就是 CPU 在 4 個以上時, 並行回收時垃圾收集執行緒最多占用不超過 25% 的 CPU 資源.

* CMS 收集器無法處理浮點垃圾 (Floating Garbage) 
CMS 收集器無法處理浮點垃圾, 可能出現 "Concurrent Mode Failure" 失敗而導致另一次 Full GC 的產生. 由於 CMS 並行清理階段用戶執行緒還在執行著, 伴隨程式的執行自然還會有新的垃圾不斷產生, 這一部分垃圾出現在標記過程之後, CMS 無法在本次收集中處理掉, 只好等待下一次 GC 時再將其清理掉.

* 演算法會產生大量空間碎片 
CMS 是一款基於 "標記-清除" 演算法實現的收集器, 這意味著收集結束時會產生大量空間碎片. 空間碎片過多會給大物件分配帶來麻煩. 往往會出現老年代還有很大的空間剩餘, 但是無法找到足夠大的連續空間來分配當前對象, 不得不提前觸發一次 Full GC. 為了解決這個問題, CMS 收集器提供一個 -XX:+UserCMSCompactAtFullCollection 開關參數, 用於在完成 Full GC 服務之後額外免費送一個磁碟重組過程, 記憶體整理的過程是無法並行的, 空間碎片的問題沒有了, 但是停頓時間卻變長了. JVM 設計者還提供另一個參數 -XX:CMSFullGCsBeforeCompaction, 這個參數用於設置在執行多少次不壓縮的 Full GC 後, 跟著來一次帶壓縮的.

- G1 收集器 
G1 (Garbage First) 收集器是當前收集器技術發展的前端技術, 在 JDK 1.6_Update 14 中提供了 Early Access 版本的 G1 收集器以供試用. 在將來的 JDK 1.7 正式發布時, G1 收集器很可能會有一個成熟的商用版本可以使用. G1 收集器與前面 CMS 收集器相比有兩個顯著改進 : 一是 G1 收集器是基於 "標記-整理" 演算法實現, 也就是說不會產生空間碎片, 這對於長時間執行的應用程式非常重要 ; 二是它可以非常精確地控制停頓, 既能讓使用者明確指定在一個長度為 M 毫秒時間片段內, 消耗在垃圾收集上的時間不得超過 N 毫秒. 

G1 會將整個 Java Heap (包括新生代, 老年代) 劃分為多個大小固定的獨立區域 (Region), 並且跟蹤這些區域裡面的垃圾堆積程度, 在後台維護一個優先列表, 每次根據允許的收集時間, 優先回收垃圾最多的區域 (這就是 First 名稱的由來). 區域劃分及有優先順序的區域回收保證了 G1 收集器在有限時間內可以獲得最高的收集效率 

垃圾收集器參數總結 : 
JDK 1.6 中的各種垃圾收集器到此已經介紹完畢, 下表整理 JVM 可使用參數提供後續研究使用 : 
 
(
 

補充說明 : 
Tomcat查看GC信息

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!