1. 程式人生 > >JVM 的垃圾回收器,你真的搞懂這些了嗎?

JVM 的垃圾回收器,你真的搞懂這些了嗎?

JVM的GC經過多年的發展,大家對 Minor GC、 major GC的理解並不完全一致,所以我不打算在本文中使用這個概念。我把GC大概分為一下4類:

Young GC:只是負責回收年輕代物件的GC;

Old GC:只是負責回收老年代物件的GC;

Full GC:回收整個堆的物件,包括年輕代、老年代、持久帶;

Mixed GC: 回收年輕代和部分老年代的GC (G1);

因為筆者目前使用G1還是比較少的,所以本文不打算將G1。

垃圾回收器演算法

目前主流垃圾回收器都採用的是可達性分析演算法來判斷物件是否已經存活,不使用引用計數演算法判斷物件時候存活的原因在於該演算法很難解決相互引用的問題。

標記-清除演算法( Mark-Sweep )

標記-清除演算法由標記階段和清除階段構成。標記階段是把所有活著的物件都做上標記的階段;清除階段是把那些沒有標記的物件,也就是非活動物件回收的階段。通過這兩個階段,就可以令不能利用的記憶體空間重新得到利用。

從 標記-清除演算法我們可以看出, 該演算法不涉及物件移動 ,但是 可能會產生記憶體碎片化 問題。空間碎片太高可能會導致程式執行時需要分配較大記憶體時候,無法找到足夠的連續記憶體,需要其他垃圾回收幫助回收記憶體。

複製演算法(Copying)

複製演算法 記憶體空間分為兩塊區域: From、to ,每次只使用其中一塊,在垃圾回收時將正在使用的記憶體中的存活物件複製到未被使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。

上面那種複製演算法有一半的空間是浪費的。所以在Java新生代把記憶體區域分為Eden空間、from、to空間3個部分, from和to空間也稱為survivor 空間,用於存放未被回收的物件 。物件開始都是 Eden生成;當回收時,將Eden和from中存活的物件移動到to區域中 。

複製演算法存在空間浪費的情況,始終都要保持一個Survivor是空閒的,並且在GC的時候要是存活物件大小超過了Survivor中的大小,就需要另外的策略儲存存活物件。

目前open JDK新生代回收策略就是採用的複製演算法,其中Eden和Survivor的預設配置為8:1

標記-壓縮演算法(Mark-Compact)

標記-壓縮演算法由標記階段和壓縮階段構成。標記階段標記-清除演算法中的標記階段完全一樣,壓縮階段是讓所有存活的物件向一端移動。這樣空閒記憶體都在另外一端,屬於連續空間,不存在記憶體碎片化問題,但是會產生物件移動。

分代演算法(Generational GC)

根據物件的不同生命週期分別管理, JVM 中將物件分為我們熟悉的新生代、老年代和永久代分別管理。這樣做的好處就是可以根據不同型別物件進行不同策略的管理,例如新生代中物件更新速度快,就會使用效率較高的複製演算法。老年代中記憶體空間相對分配較大,而且時效性不如新生代強,就會常常使用Mark-Sweep-Compact (標記-清除-壓縮)演算法。

各種演算法效能比較

常見的垃圾回收器

垃圾回收器分類

總體上可以把Java的垃圾回收器分為3類:

序列垃圾回收器(Serial Garbage Collector)

並行垃圾回收器(Parallel Garbage Collector)

併發標記掃描垃圾回收器(CMS Garbage Collector)

Java垃圾回收器主要有6種,各自優缺點以及組合關係如下:

其中的連線表示young gc和old gc可以搭配使用 

垃圾回收器選擇策略 :

客戶端程式 : Serial + Serial Old;

吞吐率優先的服務端程式(比如:計算密集型) : Parallel Scavenge + Parallel Old;

響應時間優先的服務端程式 :ParNew + CMS。

目前很大一部分的Java應用都集中在網際網路的伺服器端,這類應用尤其關係服務的響應時間,希望應用暫停時間更短,所以基本上使用的都是 ParNew + CMS ,這也是我司預設使用的配置。

CMS垃圾回收器

在啟動JVM引數加上 -XX:+UseConcMarkSweepGC ,這個引數表示對於老年代的回收採用 CMS。

CMS執行過程

CMS 的回收過程主要分為下面的幾個步驟:

初始標記(Initial Mark)

併發標記(Concurrent marking)

併發預清理(Concurrent pre-preclean)

重新標記(Final Remark)

併發清理(Concurrent sweep)

併發重置(Concurrent reset)

CMS日誌解析

標準的CMS日誌如下: 

2018-11-10T18:23:27.531+0800: 1495270.652: [GC ( CMS Initial Mark ) [1 CMS-initial-mark: 2008820K(2510848K)] 2038212K(4398336K), 0.0231086 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] 

2018-11-10T18:23:27.554+0800: 1495270.675: [CMS-concurrent-mark-start]

2018-11-10T18:23:27.644+0800: 1495270.765: [ CMS-concurrent-mark : 0.090/0.090 secs] [Times: user=0.34 sys=0.03, real=0.09 secs] 

2018-11-10T18:23:27.644+0800: 1495270.765: [CMS-concurrent-preclean-start]

2018-11-10T18:23:27.654+0800: 1495270.775: [ CMS-concurrent-preclean : 0.010/0.010 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

2018-11-10T18:23:27.655+0800: 1495270.775: [CMS-concurrent-abortable-preclean-start]

2018-11-10T18:23:32.305+0800: 1495275.425: [ CMS-concurrent-abortable-preclean :  4.623/4.650 secs] [Times: user=7.01 sys=1.01, real=4.65 secs] 

2018-11-10T18:23:32.307+0800: 1495275.427: [GC ( CMS Final Remark ) [YG occupancy: 847369 K (1887488 K)]1495275.427: [Rescan (parallel) , 0.0902177 secs]1495275.518: [weak refs processing, 0.0514433 secs]1495275.569: [class unloading, 0.0256119 secs]1495275.595: [scrub symbol table, 0.0074695 secs]1495275.602: [scrub string table, 0.0015014 secs][1 CMS-remark: 2008820K(2510848K)] 2856190K(4398336K), 0.1806988 secs] [Times: user=0.68 sys=0.00, real=0.18 secs] 

2018-11-10T18:23:32.488+0800: 1495275.609: [CMS-concurrent-sweep-start]

2018-11-10T18:23:33.660+0800: 1495276.781: [ CMS-concurrent-sweep : 1.172/1.172 secs] [Times: user=1.89 sys=0.24, real=1.17 secs] 

2018-11-10T18:23:33.661+0800: 1495276.782: [CMS-concurrent-reset-start]

2018-11-10T18:23:33.667+0800: 1495276.788: [ CMS-concurrent-reset:  0.006/0.006 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 

初始標記(CMS Initial Mark)

該階段進行可達性分析, 標記GC ROOTS能直接關聯到的物件 。 該階段會暫停應用 。

2008820K – 當前老年代使用情況;

(2510848K) – 老年代可用容量;

2038212K – 當前整個堆的使用情況;

(4398336K) – 整個堆的容量;

.0231086 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] – 時間計量;

併發標記( CMS-concurrent-mark )

併發標記就需要標記出 GC ROOTS 關聯到的物件的引用物件有哪些。比如說 A -> B (A 引用 B,假設 A 是 GC Roots 關聯到的物件),那麼這個階段就是標記出 B 物件, A 物件會在初始標記中標記出來。

併發預清理( CMS-concurrent-preclean 

這個階段主要併發查詢在做併發標記階段時從年輕代晉升到老年代的物件或老年代新分配的物件(大物件直接進入老年代)或被使用者執行緒更新的物件,來減少重新標記階段的工作量。

重新標記 ( CMS Final Remark )

由於在併發標記和併發預清理這個階段,使用者執行緒和GC 執行緒併發,假如這個階段使用者執行緒產生了新的物件,總不能被 GC 掉吧。這個階段就是為了讓這些物件重新標記。 該階段也會暫停應用

YG occupancy: 847369 K (1887488 K)]– 年輕代當前佔用情況和容量;

Rescan (parallel) , 0.0902177 secs – 這個階段在應用停止的階段完成存活物件的標記工作;

weak refs processing, 0.0514433 secs – 第一個子階段,隨著這個階段的進行處理弱引用;

class unloading, 0.0256119 secs – 第二個子階段(that is unloading the unused classes, with the duration and timestamp of the phase);

scrub symbol table, 0.0074695 secs – 最後一個子階段(that is cleaning up symbol and string tables which hold class-level metadata and internalized string respectively)

2008820K(2510848K)] – 在這個階段之後老年代佔有的記憶體大小和老年代的容量;

2856190K(4398336K) – 在這個階段之後整個堆的記憶體大小和整個堆的容量;

0.1806988 secs – 這個階段的持續時間;

[Times: user=0.68 sys=0.00, real=0.18 secs]  – 同上;

併發清理 ( CMS-concurrent-sweep )

這個階段的目的就是移除那些不用的物件,回收他們佔用的空間並且為將來使用。注意這個階段會產生新的垃圾,新的垃圾在此次GC無法清除,只能等到下次清理。這些垃圾有個專業名詞:浮動垃圾。

併發重置 ( CMS-concurrent-reset )

CMS清除內部狀態,為下次回收做準備。

注意:CMS雖然是老年代演算法,但也是需要掃描新生代區域的。

CMS演算法降級

cms存在著記憶體碎片化問題: 申請記憶體時,雖然總記憶體大於申請記憶體,但是沒有連續記憶體大於申請記憶體,導致記憶體申請失敗。 CMS提供了機制(CMS GC降級到Full GC)來解決該問題。 Full GC使用的演算法是 mark-sweep-compact( 類似於 Serial垃圾回收器), 他的作用域在 整個堆的物件,包括年輕代、老年代、持久代 , 但compaction是可選的 。其中引數 CMSFullGCsBeforeCompaction=N表示每隔N次真正的full GC才做一次壓縮 (而不是每N次CMS GC就做一次壓縮,目前JVM裡沒有這樣的引數), CMSFullGCsBeforeCompaction預設值是0,也就是每次full GC都會進行記憶體壓縮。這個儘量使用預設值,不然記憶體 碎片化可能會更嚴重些。

那麼配置的CMS GC啥時候會觸發Full gc呢?主要有下面幾種情況觸發Full Gc:

舊生代空間不足:java.lang.outOfMemoryError:java heap space;

Perm空間滿:java.lang.outOfMemoryError:PermGen space;

CMS GC時出現promotion failed(當進行 Young GC 時,有部分新生代代物件仍然可用,但是S0或S1放不下,因此需要放到老年代,但此時老年代空間無法容納這些物件) 和concurrent  mode failure(當 CMS GC 正進行時,此時有新的物件要進行老年代,但是老年代空間不足造成的);在此我向大家推薦一個架構學習交流群。交流學習群號:821169538

統計得到的minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間;

主動觸發Full GC( System.gc()、jmap等 )。

如何識別是執行的是CMS GC還是 Full GC呢?主要是根據GC log,CMS GC會在日誌中標記出各個執行階段,但是要是執行Full GC只會顯示full次數加1。

CMS相關引數

-XX:CMSInitiatingOccupancyFraction=N 和-XX:+UseCMSInitiatingOccupancyOnly

這兩個設定一般配合使用, 目的在於降低CMS GC頻率或者增加頻率。

-XX:CMSInitiatingOccupancyFraction=N 是指設定CMS在對記憶體佔用率達到N%的時候開始進行CMS GC。

-XX:+UseCMSInitiatingOccupancyOnly 只是用設定的回收閾值(上面指定的N%),如果不指定,JVM僅在第一次使用設定值,後續則自動調整.

-XX:+CMSScavengeBeforeRemark

這個引數表示CMS GC前啟動一次ygc,目的在於減少old區域對ygc區域的引用,降低remark時的開銷,一般CMS的GC耗時80%都在remark階段

-XX:+UseCMSCompactAtFullCollection和 -XX:CMSFullGCsBeforeCompaction=N

這兩個引數要配合使用,其中 CMSFullGCsBeforeCompaction上面已經講解過了。

CMS 的缺點

會產生空間碎片。CMS 垃圾回收器採用的基礎演算法是 Mark-Sweep,沒有記憶體整理的過程,所以經過 CMS 收集的堆會產生空間碎片。

對CPU資源非常敏感。為了讓應用程式不停頓,CMS 執行緒需要和應用程式執行緒併發執行,這樣就需要有更多的 CPU,同時會使得總吞吐量降低。

CMS無法處理浮動垃圾,所以一般需要更大的堆空間。因為CMS 在標記階段應用程式的執行緒還是在執行的,那麼就會有堆空間繼續分配的情況,為了保證在 CMS 回收完堆之前還有空間分配給正在執行的應用程式,必須預留一部分空間。