理解Java垃圾回收機制

理解java垃圾回收機制有什麼好處呢?作爲一個軟件工程師,滿足自己的好奇心將是一個很好的理由,不過更重要的是,理解GC工作機制可以幫助你寫出更好的Java應用程序。

這是我個人的主觀觀點,但我相信一個人精通了GC,往往會是一個更好的Java程序員。如果你對GC感興趣,那就意味着你有一定大規模應用開發的經驗。如果你已經仔細過考慮選擇合適的GC算法,這意味着你完全理解你開發的應用程序的功能。當然,這可能不是一個優秀開發者共同標準。然而,很少有人會反對我說的:理解GC是成爲一個偉大的Java開發人員的要求。

這是“成爲一個Java GC專家”系列文章的第一部分。這次我將講述GC簡介,而在下一篇文章中,我將討論分析GC狀態和來自NHN的GC調優的例子。

本文的目的是用簡單的方式向你介紹GC。我希望這篇文章被證明是非常有幫助的。事實上,我的同事已經發表了一些關於Java內部機制的大文章,它們已經在推特變得很流行。你可以去看看。

回到垃圾收集器,在學習GC前,你應該知道一個技術名詞:這個詞是“stop-the-world。“ 無論你選擇哪種GC算法,Stop-the-world都會發生。Stop-the-world意味着JVM停止應用程序,而去進行垃圾回收。當stop-the-world發生時,除了進行垃圾回收的線程,其他所有線程都將停止運行。被中斷的任務將在GC任務完成後恢復執行。GC調優往往意味着減少stop-the-world的時間。

分代垃圾收集

在Java代碼中,Java語言沒有顯式的提供分配內存和刪除內存的方法。一些開發人員將引用對象設置爲null或者調用System.gc()來釋放內存。將引用對象設置爲null沒有什麼大問題,但是調用system.gc()方法會大大的影響系統性能,絕對不能這個幹。(謝天謝地,我還沒看到任何NHN開發者調用這個方法。)

在Java中,由於開發人員沒有在代碼中顯式刪除內存,所以垃圾收集器會去發現不需要(垃圾)的對象,然後刪除它們,釋放內存。這款垃圾收集器是基於以下兩個假設而創建的。(稱他們爲前提條件更好,而不是假設。)

  • 絕大多數對象在短時間內變得不可達
  • 只有少量年老對象引用年輕對象.

這些假設被稱爲“弱代假說”。爲了發揮這一假設的優勢,在HotSpot虛擬機中,物理的將內存分爲兩個—年輕代(young generation)老年代(old generation)

年輕代:新創建的對象都存放在這裏。因爲大多數對象很快變得不可達,所以大多數對象在年輕代中創建,然後消失。當對象從這塊內存區域消失時,我們說發生了一次“minor GC”。

年代:沒有變得不可達,存活下來的年輕代對象被複制到這裏。這塊內存區域一般大於年輕代。因爲它更大的規模,GC發生的次數比在年輕代的少。對象從老年代消失時,我們說“major GC”(或“full GC”)發生了。

我們看一下這幅圖。

圖 1: GC區 & 數據流

上圖中的永久代(permanent generation)也稱爲“方法區(method area)”,他存儲class對象和字符串常量。所以這塊內存區域絕對不是永久的存放從老年代存活下來的對象的。在這塊內存中有可能發生垃圾回收。發生在這裏垃圾回收也被稱爲major GC。

一些人可能想知道:

一個老年代的對象需要引用年輕代的對象,該怎麼辦?

爲了解決這些問題,老年代中有一個被稱爲“卡表(card table)”的東西,它是一個512 byte大小的塊。每當老年代的對象引用年輕代對象時,這種引用會被記錄在這張表格中。當垃圾回收發生在年輕代時,只需對這張表進行搜索以確定是否需要進行垃圾回收,而不是檢查老年代中的所有對象引用。這張表格用一個叫做“寫閘(write barrier)”的東西進行管理。“寫閘”是一種裝置,對minor GC有更好性能。雖然因爲這種機制,會產生一些時間性能開銷,但降低了整體的GC時間。

圖2: Card Table結構

年輕代組成部分

爲了理解GC,我們學習一下年輕代,對象第一次創建發生在這塊內存區域。年輕代分爲3塊。

  • Eden區
  • 2個Survivor

年輕代總共有3塊空間,其中2塊爲Survivor區。各個空間的執行順序如下:

  1. 絕大多數新創建的對象分配在Eden區。
  2. 在Eden區發生一次GC後,存活的對象移到其中一個Survivor區。
  3. 在Eden區發生一次GC後,對象是存放到Survivor區,這個Survivor區已經存在其他存活的對象。
  4. 一旦一個Survivor區已滿,存活的對象移動到另外一個Survivor區。然後之前那個空間已滿Survivor區將置爲空,沒有任何數據。
  5. 經過重複多次這樣的步驟後依舊存活的對象將被移到老年代。

通過檢查這些步驟,如你看到的樣子,其中一個Survivor區必須保持空。如果數據存在於兩個Survivor區,或兩個都沒使用,你可以將這個情況作爲系統錯誤的一個標誌。

經過多次minor GC,數據被轉移到老年代過程如下面的圖表所示:

圖3: GC前和GC後

請注意,在HotSpot虛擬機中,使用兩種技術加快內存的分配。一個被稱爲“指針碰撞(bump-the-pointer)”,另外一個被稱爲“TLABs(線程本地分配緩衝)”。

指針碰撞技術跟蹤分配給Eden區上最新的對象。該對象將位於Eden 區的頂部。如果之後有一個對象被創建,只需檢查Eden區是否有足夠大的空間存放該對象。如果空間夠用,它將被放置在Eden區,存放在空間的頂部。因此,在創建新對象時,只需檢查最後被添加對象,看是否還有更多的內存空間允許分配。然而,如果考慮多線程的環境,則是另外一種情況。爲了實現多線程環境下,在Eden 區線程安全的去創建保存對象,那麼必須加鎖,因此性能會下降。在HotSpot虛擬機中TLABs能夠解決這一問題。它允許每個線程在Eden區有自己的一小塊私有空間。因爲每一個線程只能訪問自己的TLAB,所以在這個區域甚至可以使用無鎖的指針碰撞技術進行內存分配。

我們已經對年輕代有了一個快速的瀏覽。你不需要要記住我剛纔提到的兩種技術。即便你不知道他們,也不會怎麼樣。但請務必記住:對象第一次被創建發生在Eden區,長期存活的對象被移動到老年代的Survivor區。

老年代GC

當老年代數據滿時,基本上會執行一次GC。執行程序根據不同GC類型而變化,所以如果你知道不同類型的垃圾收集器,會更容易理解垃圾回收過程。

在JDK7中,有5種垃圾收集器:

  1. Serial收集器
  2. Parallel收集器
  3. Parallel Old收集器 (Parallel Compacting GC)收集器
  4. Concurrent Mark & Sweep GC  (or “CMS”)收集器
  5. Garbage First (G1) 收集器

其中,serial 收集器一定不能用於服務器端。這個收集器類型僅應用於單核CPU桌面電腦。使用serial收集器會顯着降低應用程序的性能。

現在讓我們來了解每個收集器類型。

Serial 收集器 (-XX:+UseSerialGC)

我們在前一段的解釋了在年輕代發生的垃圾回收算法類型。在老年代的GC使用算法被稱爲“標記-清除-整理”。

  1. 該算法的第一步是在老年代標記存活的對象。
  2. 從頭開始檢查堆內存空間,並且只留下依然倖存的對象(清除)。
  3. 最後一步,從頭開始,順序地填滿堆內存空間,將存活的對象連續存放在一起,這樣堆分成兩部分:一邊有存放的對象,一邊沒有對象(整理)。

serial收集器應用於小的存儲器和少量的CPU。

 

Parallel收集器(-XX:+UseParallelGC)

圖4: Serial收集器 和 Parallel收集器的差異 

從這幅圖中,你可以很容易看到Serial收集器 和 Parallel收集器的差異。serial收集器只使用一個線程來處理的GC,而parallel收集器使用多線程並行處理GC,因此更快。當有足夠大的內存和大量芯數時,parallel收集器是有用的。它也被稱爲“吞吐量優先垃圾收集器。”

Parallel Old 垃圾收集器(-XX:+UseParallelOldGC)

Parallel Old收集器是自JDK 5開始支持的。相比於parallel收集器,他們的唯一區別就是在老年代所執行的GC算法的不同。它執行三個步驟:標記-彙總-壓縮(mark – summary – compaction)。彙總步驟與清理的不同之處在於,其將依然倖存的對象分發到GC預先處理好的不同區域,算法相對清理來說略微複雜一點。

CMS GC (-XX:+UseConcMarkSweepGC)

圖5: Serial GC & CMS GC

 

CMS垃圾收集器(-XX:+UseConcMarkSweepGC)

如你在上圖看到的那樣, CMS垃圾收集器比之前我解釋的各種算法都要複雜很多。初始標記(initial mark) 比較簡單。這一步驟只是查找距離類加載器最近的倖存對象。所以停頓時間非常短。之後的併發標記步驟,所有被倖存對象引用的對象會被確認是否已經被追蹤檢查。這一步的不同之處在於,在標記的過程中,其他的線程依然在執行。在重新標記步驟會修正那些在併發標記步驟中,因新增或者刪除對象而導致變動的那部分標記記錄。最後,在併發清除步驟,垃圾收集器執行。垃圾收集器進行垃圾收集時,其他線程的依舊在工作。一旦採取了這種GC類型,由於垃圾回收導致的停頓時間會極其短暫。CMS 收集器也被稱爲低延遲垃圾收集器。它經常被用在那些對於響應時間要求十分苛刻的應用上。

當然,這種GC類型在擁有stop-the-world時間很短的優點的同時,也有如下缺點:

  •  它會比其他GC類型佔用更多的內存和CPU
  •  默認情況下不支持壓縮步驟

在使用這個GC類型之前你需要慎重考慮。如果因爲內存碎片過多而導致壓縮任務不得不執行,那麼stop-the-world的時間要比其他任何GC類型都長,你需要考慮壓縮任務的發生頻率以及執行時間。

G1 GC

最後,我們來學習一下G1類型。

圖6: Layout of G1 GC

如果你想要理解G1收集器,首先你要忘記你所理解的新生代和老年代。正如你在上圖所看到的,每個對象被分配到不同的網格中,隨後執行垃圾回收。當一個區域填滿之後,對象被轉移到另一個區域,並再執行一次垃圾回收。在這種垃圾回收算法中,不再有從新生代移動到老年代的三部曲。這個類型的垃圾收集算法是爲了替代CMS 收集器而被創建的,因爲CMS 收集器在長時間持續運行時會產生很多問題。

G1最大的好處是他的性能,他比我們在上面討論過的任何一種GC都要快。但是在JDK 6中,他還只是一個早期試用版本。在JDK7之後才由官方正式發佈。就我個人看來,NHN在將JDK 7正式投入商用之前需要很長的一段測試期(至少一年)。因此你可能需要再等一段時間。並且,我也聽過幾次使用了JDK 6中的G1而導致Java虛擬機宕機的事件。請耐心的等待它更穩定吧。

下一次我將討論GC優化相關問題,但是在此之前我要先明確一件事情。假如應用中創建的所有對象的大小和類型都是統一的,在我們公司,這種情下使用的WAS的GC參數 可以是相同的。但是WAS所創建對象的大小和生命週期根據服務以及硬件的不同而不同。換句話說,不能因爲某個應用使用的GC參數“A”,就說明同樣的參數 也能給其他服務帶來最佳效果。而是要因地制宜,有的放矢。我們需要找到適合每個WAS線程的最佳參數,並且持續的監控和優化每個設備上的WAS實例。這並不是我的一家之談,而是負責Oracle Java虛擬機研發的工程師在 JavaOne 2010上已經討論過的。

本文中我們簡略的介紹了Java的垃圾回收機制,請繼續關注我們的後續文章,我們將會討論如何監控Java GC狀態以及GC調優。

另外,我特別推薦一本2011年12月發佈的《Java性能》(Amazon,也可以通過safari在線閱讀),還有在Oracle官網發佈的白皮書《Java HotSpotTM虛擬機內存管理》(這本書與Java性能優化不是同一本)

作者Sangmin Lee, NHN公司,性能設計實驗室高級工程師。

可參考:http://blog.jobbole.com/80499/


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章