[譯]GC專家系列1:理解Java垃圾回收

https://segmentfault.com/a/1190000004233812程序員

 

瞭解Java的垃圾回收(GC)原理能給咱們帶來什麼好處?對於軟件工程師來講,知足技術好奇心可算是一個,但重要的是理解GC能幫忙咱們更好的編寫Java應用程序。算法

上面是我我的的主觀的見解,但我相信熟練掌握GC是成爲優秀Java程序員的必備技能。若是你對GC執行過程感興趣,也許你只是有必定的開發應用的經驗;若是你仔細考慮過如何選擇合適的GC算法,說明你對你所開發的程序有了全面的瞭解。固然這對一個優秀的程序員來講未必是一個通用的標準,但不多人會反對我關於"理解GC是做爲優秀Java程序員的必備技能"的見解。segmentfault

本文是成爲Java GC專家系列的第一篇。我將先對GC作一下基本的概述,在下一篇文章中,我將講述如何分析GC狀態以及經過[NHN]()的案例介紹GC調優相關的內容。安全

本文的目的是以通俗的方式爲你介紹GC概念。我但願本文會對你有所幫助。事實上,個人同事們已經發表了一些在Twitter上很是受關注的優秀文章,你一樣也能夠拿來參考。服務器

回到垃圾回收上,在開始學習GC以前你應該知道一個詞:stop-the-world。無論選擇哪一種GC算法,stop-the-world都是不可避免的。Stop-the-world意味着從應用中停下來並進入到GC執行過程當中去。一旦Stop-the-world發生,除了GC所需的線程外,其餘線程都將中止工做,中斷了的線程直到GC任務結束才繼續它們的任務。GC調優一般就是爲了改善stop-the-world的時間。多線程

基於的分代理論的垃圾回收

在Java程序裏不須要顯式的分配和釋放內存。有些人經過給對象賦值爲null或調用System.gc()以指望顯式的釋放內存空間。給對象設置null雖沒什麼用,但問題不會太大;若是調用了System.gc()卻可能會爲系統性能帶來嚴重的波動,即使調用System.gc()系統也未必當即響應去執行垃圾回收。(所幸的是,在NHN不曾看到有工程師這麼作。)併發

在使用Java時,程序員不須要在程序代碼中顯式的釋放內存空間,垃圾回收器會幫你找到再也不須要的(垃圾)對象並把他們移出。垃圾回收器的建立基於如下兩個假設(也許稱之爲推論或前提更合適):佈局

  • 大多數對象的很快就會變得不可達性能

  • 只有極少數狀況會出現舊對象持有新對象的引用學習

這兩條假設被稱爲"弱分代假設"。爲了證實此假設,在HotSpot VM中物理內存空間被劃分爲兩部分:新生代(young generate)老年代(old generation)

新生代:大部分的新建立對象分配在新生代。由於大部分對象很快就會變得不可達,因此它們被分配在新生代,而後消失再也不。當對象重新生代移除時,咱們稱之爲"minor GC"。

老年代:存活在新生代中但未變爲不可達的對象會被複制到老年代。通常來講老年代的內存空間比新生代大,因此在老年代GC發生的頻率較新生代低一些。當對象從老年代被移除時,咱們稱之爲"major GC"(或者full GC)。

看一下下圖的示意:


圖1:GC區域和數據流向

圖中的permanent generation稱爲方法區,其中存儲着類和接口的元信息以及interned的字符串信息。因此這一區域並非爲老年代中存活下來的對象所定義的持久區。方法區中也會發生GC,這裏的GC一樣也被稱爲major GC

有些人可能認爲:

若是老年代的對象須要持有新生代對象的引用怎麼辦?

爲了處理這種場景,在老年代中設計了"索引表(card table)",是一個512字節的數據塊。無論什麼時候老年代須要持有新生代對象的引用時,都會記錄到此表中。當新生代中須要執行GC時,經過搜索此表決定新生代的對象是否爲GC的目標對象,從而下降遍歷全部老年代對象進行檢查的代價。該索引表使用寫柵欄(write barrier)進行管理。wite barrier是一個容許高性能執行minor GC的設備。儘管它會引入必定的開銷,卻能帶來整體GC時間的大幅下降。


圖2:索引表結構

新生代的結構

爲了深刻理解GC,咱們先重新生代開始學起。全部的對象在初始建立時都會被分配在新生代中。新生代又可分爲三個部分:

  • 一個Eden

  • 兩個Survivor

在三個區域中有兩個是Survivor區。對象在三個區域中的存活過程以下:

  1. 大多數新生對象都被分配在Eden區。

  2. 第一次GC事後Eden中還存活的對象被移到其中一個Survivor區。

  3. 再次GC過程當中,Eden中還存活的對象會被移到以前已移入對象的Survivor區。

  4. 一旦該Survivor區域無空間可用時,還存活的對象會從當前Survivor區移到另外一個空的Survivor區。而當前Survivor區就會再次置爲空狀態。

  5. 通過數次在兩個Survivor區域移動後還存活的對象最後會被移動到老年代。

如上所述,兩個Survivor區域在任什麼時候候一定有一個保持空白。若是同時有數據存在於兩個Survivor區或者兩個區域的的使用量都是0,則意味着你的系統可能出現了運行錯誤。

下圖向你展現了通過minor GC把數據遷移到老年代的過程:


圖3: GC先後

在HotSpot VM中,使用了兩項技術來實現更快的內存分配:"指針碰撞(bump-the-pointer)"和"TLABs(Thread-Local Allocation Buffers)"。

Bump-the-pointer技術會跟蹤在Eden上新建立的對象。因爲新對象被分配在Eden空間的最上面,因此後續若是有新對象建立,只須要判斷新建立對象的大小是否知足剩餘的Eden空間。若是新對象知足要求,則其會被分配到Eden空間,一樣位於Eden的最上面。因此當有新對象建立時,只須要判斷此新對象的大小便可,所以具備更快的內存分配速度。然而,在多線程環境下,將會有別樣的情況。爲了知足多個線程在Eden空間上建立對象時的線程安全,不可避免的會引入鎖,所以隨着鎖競爭的開銷,建立對象的性能也大打折扣。在HotSpot中正是經過TLABs解決了多線程問題。TLABs容許每一個線程在Eden上有本身的小片空間,線程只能訪問其本身的TLAB區域,所以bump-the-pointer能經過TLAB在不加鎖的狀況下完成快速的內存分配。

本小節快速瀏覽了新生代上的GC知識。上面講的兩項技術無需刻意記憶,只須要明白對象開始是建立在Eden區,而後通過在Survivor區域上的數次轉移而存活下來的長壽對象最後會被移到老年代。

老年代垃圾回收

當老年代數據滿時,便會執行老年代垃圾回收。根據GC算法的不一樣其執行過程也會有所區別,因此當你瞭解了每種GC的特色後再來理解老年代的垃圾回收就會容易不少。

在JDK 7中,內置了5種GC類型:

  1. Serial GC

  2. Parallel GC

  3. Parallel Old GC(Parallel Compacting GC)

  4. Concurrent Mark & Sweep GC (or "CMS")

  5. Garbage First (G1) GC

其中Serial GC務必不要在生產環境的服務器上使用,這種GC是爲單核CPU上的桌面應用設計的。使用Serial GC會明顯的損耗應用的性能。

下面分別介紹每種GC的特性。

Serial GC(-XX:+UseSerialGC)

在前面介紹的年輕代垃圾回收中使用了這種類型的GC。在老年代,則使用了一種稱之爲"mark-sweep-compact"的算法。

  1. 首先該算法須要在老年代中標記出存活着的對象

  2. 而後從前到後檢查堆空間中存活的對象,並保持位置不變(把再也不存活的對象清理出堆空間,稱爲空間清理)

  3. 最後,把存活的對象移到堆空間的前面部分以保持已使用的堆空間的連續性,從而把堆空間分爲兩部分:有對象的和無對象的(稱爲空間壓縮)

Serial GC適用於CPU核數較少且使用的內存空間較小的場景。

Parallel GC(-XX:+UseParallelGC)


圖4:Serial GC與Parallel GC的區別

圖中能夠容易的看出serial GC與parallel GC的區別。Serial GC使用單一線程執行GC,而parallel GC則使用多個線程併發執行,所以parallel GC 較serial GC具備更快的速度。Parallel GC適用於多核CPU且使用了較大內存空間的場景。Parallel GC又被稱爲"高吞吐GC(throughput GC)"

Parallel Old GC(-XX:+UseParallelOldGC)

Parallel Old GC在JDK 5中被引入,與Parallel GC相比惟一的區別在於Parallel的GC算法是爲老年代設計的。它的執行過程分爲三步:標記(mark)--總結(summary)--壓縮(compaction)。其中summary步驟會會分別爲存活的對象在已執行過GC的空間上標出位置,所以與mark-sweep-compact算法中的sweep步驟有所區別,並須要一些複雜步驟才能完成。

CMS GC(-XX:+UseConcMarkSweepGC)


圖5:Serial GC與CMS GC

從圖上可看出併發標記-清理(Concurrent Mark-Sweep) GC比之後上其餘GC都要複雜。開始時的初始標記(initial mark)比較簡單,只有靠近類加載器的存活對象會被標記,所以停頓時間(stop-the-world)比較短暫。在併發標記(concurrent mark)階段,由剛被確認和標記過的存活對象所關聯的對象將被會跟蹤和檢測存活狀態。此步驟的不一樣之處在於有多個線程並行處理此過程。在重標記(remark)階段,由併發標記所關聯的新增或停止的對象瘵被會檢測。在最後的併發清理(concurrent sweep)階段,垃圾回收過程被真正執行。在垃圾回收執行過程當中,其餘線程依然在執行。得益於CMS GC的執行方式,在GC期間系統中斷時間很是短暫。CMS GC也被稱爲低延遲GC,適用於全部應用對響應時間要求比較嚴格的場景

CMS GC雖然具備中斷時間斷的優點,其缺點也比較明顯:

  • 與其餘GC相比,CMS GC要求更多的內存空間和CPU資源

  • CMS GC默認不提供內存壓縮

使用CMS GC以前須要對系統作全面的分析。另外爲了不過多的內存碎片而須要執行壓縮任務時,CMS GC會比任何其餘GC帶來更多的stop-the-world時間,因此你須要分析和判斷壓縮任務執行的頻率及其耗時狀況。

G1 GC

最後咱們學習有關G1垃圾回收的介紹。


圖6:G1 GC的佈局

若是你想清晰的理解GC,請先忘記上面介紹的有關新生代和老年代的知識。如上圖所示,每一個對象在建立時會分析到一個格子中,後續的GC也是在格子中完成的。每當一個區域分配滿對象後,新建立的對象就會分配到另一個區域,並開始執行GC。在這種GC中不會出現其餘GC中的對象在新生代和老生代三區域中移動的現象。G1是爲了取代在長期使用中暴露出大量問題且飽受抱怨的CMS GC。

G1最大的改進在於其性能表現,它比以上任何一種GC都更快速。它在JDK6中以早期版本的形式釋放出來以用於測試,它真正的發佈是在JDK7中。我我的認爲在NHN真正在生產環境使用JDK7至少還須要1年的測試時間,因此還須要等待一段時間。而且我據說在JDK6中使用G1偶爾會出現JVM崩潰現象。因此穩定版尚需時日。

接下來的文章中會講解GC調優,但我想先提一個問題。若是應用中全部對象的類型和大小都是同樣的,WAS上使用的GC能夠設置相同的GC選項。若是在WAS上建立的對象的大小和生命週期各不相同的對象,配置的GC選項也各不相同。換名話說,不能由於一個服務使用了GC選項"A",其餘的不一樣服務使用相同的選項"A"也能獲取最好的表現。因此爲了找到WAS線程的最佳值,每一個WAS實例須要經過持續的調優和監控以便找到最優的配置和GC優項。這不僅是來自個人我的經驗,而是來自於JavaOne 2010上工程師們對於Oracle JVM討論後的一致見解。

相關文章
相關標籤/搜索