理解Java垃圾回收

在開始學習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)。

看一下下圖的示意:


GC區域和數據流向

圖中的permanent generation稱為方法區,其中存儲著類和接口的元信息以及interned的字符串信息。所以這一區域并不是為老年代中存活下來的對象所定義的持久區。方法區中也會發生GC,這里的GC同樣也被稱為major GC

有些人可能認為:

如果老年代的對象需要持有新生代對象的引用怎么辦?

為了處理這種場景,在老年代中設計了"索引表(card table)",是一個512字節的數據塊。不管何時老年代需要持有新生代對象的引用時,都會記錄到此表中。當新生代中需要執行GC時,通過搜索此表決定新生代的對象是否為GC的目標對象,從而降低遍歷所有老年代對象進行檢查的代價。該索引表使用寫柵欄(write barrier)進行管理。wite barrier是一個允許高性能執行minor GC的設備。盡管它會引入一定的開銷,卻能帶來總體GC時間的大幅降低。

索引表結構

新生代的結構

為了深入理解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把數據遷移到老年代的過程:


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區域上的數次轉移而存活下來的長壽對象最后會被移到老年代

新生代垃圾回收

在新生代中,使用“停止-復制”算法進行清理,將新生代內存分為2部分,1部分 Eden區較大,1部分Survivor比較小,并被劃分為兩個等量的部分。每次進行清理時,將Eden區和一個Survivor中仍然存活的對象拷貝到 另一個Survivor中,然后清理掉Eden和剛才的Survivor。

這里也可以發現,停止復制算法中,用來復制的兩部分并不總是相等的(傳統的停止復制算法兩部分內存相等,但新生代中使用1個大的Eden區和2個小的Survivor區來避免這個問題)

由于絕大部分的對象都是短命的,甚至存活不到Survivor中,所以,Eden區與Survivor的比例較大,HotSpot默認是 8:1,即分別占新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的內存超過了10%,則需要將一部分對象分配到 老年代。用-XX:SurvivorRatio參數來配置Eden區域Survivor區的容量比值,默認是8,代表Eden:Survivor1:Survivor2=8:1:1.

老年代垃圾回收

老年代用的算法是標記-整理算法,即:標記出仍然存活的對象(存在引用的),將所有存活的對象向一端移動,以保證內存的連續。

在發生Minor GC時,虛擬機會檢查每次晉升進入老年代的大小是否大于老年代的剩余空間大小,如果大于,則直接觸發一次Full GC,否則,就查看是否 設置了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍內存分配失敗;如果不允許,則仍然進行Full GC(這代表著如果設置-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多內存,所以,最好不要這樣做)。

當老年代數據滿時,便會執行老年代垃圾回收。根據GC算法的不同其執行過程也會有所區別,所以當你了解了每種GC的特點后再來理解老年代的垃圾回收就會容易很多。

垃圾收集器

在GC機制中,起重要作用的是垃圾收集器,垃圾收集器是GC的具體實現,Java虛擬機規范中對于垃圾收集器沒有任何規定,所以不同廠商實現的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下圖(圖來源于《深入理解Java虛擬機:JVM高級特效與最佳實現》,圖中兩個收集器之間有連線,說明它們可以配合使用):

在介紹垃圾收集器之前,需要明確一點,就是在新生代采用的停止復制算法中,“停 止(Stop-the-world)”的意義是在回收內存時,需要暫停其他所 有線程的執行。這個是很低效的,現在的各種新生代收集器越來越優化這一點,但仍然只是將停止的時間變短,并未徹底取消停止。

  • Serial收集器:新生代收集器,使用停止復制算法,使用一個線程進行GC,串行,其它工作線程暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行內存回收(這也是虛擬機在Client模式下運行的默認值)

  • ParNew收集器:新生代收集器,使用停止復制算法,Serial收集器的多線程版,用多個線程進行GC,并行,其它工作線程暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集內存;使用-XX:ParallelGCThreads來設置執行內存回收的線程數。

  • Parallel Scavenge 收集器:新生代收集器,使用停止復制算法,關注CPU吞吐量,即運行用戶代碼的時間/總時間,比如:JVM運行100分鐘,其中運行用戶代碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合運行后臺運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合用戶交互,提高用戶體驗)。使用-XX:+UseParallelGC開關控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的默認值);使用-XX:GCTimeRatio來設置用戶執行時間占總時間的比例,默認99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設置GC的最大停頓時間(這個參數只對Parallel Scavenge有效),用開關參數-XX:+UseAdaptiveSizePolicy可以進行動態控制,如自動調整Eden/Survivor比例,老年代對象年齡,新生代大小等,這個參數在ParNew下沒有。

  • Serial Old收集器:老年代收集器,單線程收集器,串行,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象干掉,只留幸存的對象,壓縮是將移動對象,將空間填滿保證內存分為2塊,一塊全是對象,一塊空閑)算法,使用單線程進行GC,其它工作線程暫停(注意,在老年代中進行標記整理算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。

  • Parallel Old收集器:老年代收集器,多線程,并行,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這里的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將幸存的對象復制到預先準備好的區域,而不是像Sweep(清理)那樣清理廢棄的對象)算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現后(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。

  • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力于獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標記清除算法,多線程,優點是并發收集(用戶線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行內存回收,優先使用ParNew+CMS(原因見后面),當用戶線程內存不足時,采用備用方案Serial Old收集。CMS收集的執行過程是:初始標記(CMS-initial-mark) -> 并發標記(CMS-concurrent-mark) -->預清理(CMS-concurrent-preclean)-->可控預清理(CMS-concurrent-abortable-preclean)-> 重新標記(CMS-remark) -> 并發清除(CMS-concurrent-sweep) ->并發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)

在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)

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)

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垃圾回收的介紹。


G1 GC的布局

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

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

注意并發(Concurrent)和并行(Parallel)的區別:

    并發是指用戶線程與GC線程同時執行(不一定是并行,可能交替,但總體上是在同時執行的),不需要停頓用戶線程(其實在CMS中用戶線程還是需要停頓的,只是非常短,GC線程在另一個CPU上執行);  

    并行收集是指多個GC線程并行工作,但此時用戶線程是暫停的;  

所以,Serial是串行的,Parallel收集器是并行的,而CMS收集器是并發的.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容