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

JVM的GC通過多年的發展,你們對 Minor GC、 major GC的理解並不徹底一致,因此我不打算在本文中使用這個概念。我把GC大概分爲一下4類:java

Young GC:只是負責回收年輕代對象的GC;算法

Old GC:只是負責回收老年代對象的GC;服務器

Full GC:回收整個堆的對象,包括年輕代、老年代、持久帶;架構

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

由於筆者目前使用G1仍是比較少的,因此本文不打算將G1。性能

垃圾回收器算法學習

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

標記-清除算法( 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 回收完堆以前還有空間分配給正在運行的應用程序,必須預留一部分空間。

相關文章
相關標籤/搜索