標籤:分享 消失 裝置 優秀 部分 指標 技術分享 快速 軟體工程師
stop-the-world
原文連結:http://www.cubrid.org/blog/de...
瞭解Java的記憶體回收(GC)原理能給我們帶來什麼好處?對於軟體工程師來說,滿足技術好奇心可算是一個,但重要的是理解GC能幫忙我們更好的編寫Java應用程式。
上面是我個人的主觀的看法,但我相信熟練掌握GC是成為優秀Java程式員的必備技能。如果你對GC執行過程感興趣,也許你只是有一定的開發應用的經驗;如果你仔細考慮過如何選擇合適的GC演算法,說明你對你所開發的程式有了全面的瞭解。當然這對一個優秀的程式員來說未必是一個通用的標準,但很少人會反對我關於"理解GC是作為優秀Java程式員的必備技能"的看法。
本文是成為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,我們先從新生代開始學起。所有的對象在初始建立時都會被分配在新生代中。新生代又可分為三個部分:
在三個地區中有兩個是Survivor區。對象在三個地區中的存活過程如下:
大多數新生對象都被分配在Eden區。
第一次GC過後Eden中還存活的對象被移到其中一個Survivor區。
再次GC過程中,Eden中還存活的對象會被移到之前已移入對象的Survivor區。
一旦該Survivor地區無空間可用時,還存活的對象會從當前Survivor區移到另一個空的Survivor區。而當前Survivor區就會再次置為空白狀態。
經過數次在兩個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類型:
Serial GC
Parallel GC
Parallel Old GC(Parallel Compacting GC)
Concurrent Mark & Sweep GC (or "CMS")
Garbage First (G1) GC
其中Serial GC務必不要在生產環境的伺服器上使用,這種GC是為單核CPU上的案頭應用設計的。使用Serial GC會明顯的損耗應用的效能。
下面分別介紹每種GC的特性。
Serial GC(-XX:+UseSerialGC)
在前面介紹的年輕代記憶體回收中使用了這種類型的GC。在老年代,則使用了一種稱之為"mark-sweep-compact"的演算法。
首先該演算法需要在老年代中標記出存活著的對象
然後從前到後檢查堆空間中存活的對象,並保持位置不變(把不再存活的對象清理出堆空間,稱為空白間清理)
最後,把存活的對象移到堆空間的前面部分以保持已使用的堆空間的連續性,從而把堆空間分為兩部分:有對象的和無對象的(稱為空白間壓縮)
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雖然具有停機時間斷的優勢,其缺點也比較明顯:
使用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討論後的一致看法。
本節我們只簡單介紹Java中的GC基礎。下一章節,我將會討論關於如何監控GC狀態以及如何做效能調優。
本文參考了2011年12月出版的《Java 效能》和Oracle網站上提供的白皮書《Java HotspotTM 虛擬機器記憶體管理》。
Sangmin Lee, 效能實驗室進階工程師,NHN公司
理解Java記憶體回收