天天看點

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器

  • 一、新生代的垃圾回收算法:複制算法
    • 1.1 複制算法的缺點?
    • 1.2 複制算法的優化:Eden區和Survivor區
  • 二、老年代的垃圾回收算法
    • 2.1 新生代裡的對象一般在什麼場景下會進入老年代?
    • 2.2 Minor GC後的對象太多無法放入Survivor區怎麼辦?
    • 2.3 老年代空間配置設定擔保規則
    • 2.4 老年代垃圾回收觸發時機
    • 2.5 老年代垃圾回收算法:标記整理算法
    • 一個日處理上億資料的計算系統的JVM 優化分析過程
  • 三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?
    • JVM的痛點:Stop the World
    • 3.1 最常用的新生代垃圾回收器:ParNew
      • 如何為線上系統指定使用ParNew垃圾回收器?
      • ParNew垃圾回收器預設情況下的線程數量
    • 思考題:到底是用單線程垃圾回收好,還是多線程垃圾回收好? 到底是Serial垃圾回收器好還是ParNew垃圾回收器好?
    • 3.2 老年代常用的垃圾回收器:CMS
      • 3.2.1 CMS如何實作系統一邊工作的同時進行垃圾回收?
      • 3.2.2 CMS垃圾回收的細節問題
    • 思考題:為什麼老年代的垃圾回收速度會比新生代的垃圾回收速度慢很多倍?到 底慢在哪裡?
    • 案例優化分析:
      • 1.一個日處理上億資料的計算系統的JVM優化案例分析。
      • 2. 每日上億請求量的電商系統的優化。
    • 面試題:ParNew + CMS 的GC ,如何保證隻做young GC,JVM 參數如何配置?
  • 四、 G1 垃圾回收器
    • 4.1 ParNew + CMS帶給我們的痛點是什麼?
    • 4.2 G1垃圾回收器
    • 4.3 G1是如何做到對垃圾回收導緻的系統停頓可控的?
    • 4.4 如何設定G1對應的記憶體大小?
    • 4.5 G1的新生代垃圾回收
    • 4.6 對象什麼時候進入老年代?
    • 4.7 大對象Region
    • 4.8 新生代+老年代的混合垃圾回收的觸發時機?
    • 4.9 G1垃圾回收的過程
    • 4.10 G1垃圾回收器的一些參數
    • 4.11 回收失敗時的Full GC
    • 4.12 百萬級使用者的線上教育平台,如何基于G1垃圾優化垃圾回收性能?
  • 五、相關名詞解釋

一、新生代的垃圾回收算法:複制算法

所謂的“複制算法“,把新生代記憶體劃分為兩塊記憶體區域,然後隻使用其中一塊記憶體,待那塊記憶體快滿的時候,就把裡面的存活對象一次性轉移到另外一塊記憶體區域,保證沒有記憶體碎片,接着一次性回收原來那塊記憶體區域的垃圾對象,再次空出來一塊記憶體區域。兩塊記憶體區域就這麼重複着循環使用。

1.1 複制算法的缺點?

複制算法的缺點其實非常的明顯,如果按照上述的思路,假設我們給新生代1G的記憶體空間,那麼隻有 512MB的記憶體空間是可以用的。

另外512MB的記憶體空間是一直要放在那裡空着的,然後512MB記憶體空間滿了,就把存活對象轉移到另外一塊512MB 的記憶體空間去

從始至終,就隻有一半的記憶體可以用,這樣的算法顯然對記憶體的使用效率太低了。

1.2 複制算法的優化:Eden區和Survivor區

實際上真正的複制算法會做出如下優化,把新生代記憶體區域劃分為三塊:

1個 Eden 區,2個 Survivor 區,其中 Eden 區占80%記憶體空間,每一塊 Survivor 區各占10%記憶體空間,比如說 Eden 區有800MB記憶體,每 一塊Survivor區就100MB記憶體,如下圖。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

平時可以使用的,就是 Eden 區和其中一塊 Survivor 區,那麼相當于就是有900MB的記憶體是可以使用的。

剛開始對象都是配置設定在Eden區内的,如果Eden區快滿了,此時就會觸發垃圾回收。此時就會把Eden區中的存活對象都一次性轉移到一塊空着的Survivor區。接着Eden區就會被清空,然後再次配置設定新對象到Eden區裡,Eden區和一塊Survivor區裡是有對象的,其中Survivor區裡放的是上一次 Minor GC後存活的對象。

如果下次再次 Eden 區滿,那麼再次觸發 Minor GC,就會把 Eden 區和放着上一次 Minor GC後存活對象的 Survivor 區内的存活對象,轉移到另外一塊Survivor區去。

接着新對象繼續配置設定在Eden區和另外那塊開始被使用的Survivor區,然後始終保持一塊Survivor區是空着的,就這樣一直循環使用這三塊記憶體區域。

最大的好處,就是隻有10%的記憶體空間是被閑置的,90%的記憶體都被使用上了

無論是垃圾回收的性能,記憶體碎片的控制,還是說記憶體使用的效率,都得到優化。

二、老年代的垃圾回收算法

2.1 新生代裡的對象一般在什麼場景下會進入老年代?

  1. 躲過15次GC之後進入老年代

    對象每次在新生代裡躲過一次GC被轉移到一塊 Survivor 區域中,此時他的年齡就會增長一歲。預設的設定下,當對象的年齡達到15歲的時候,也就是躲過15次GC的時候,他就會轉移到老年代裡去。

    這個具體是多少歲進入老年代,可以通過JVM參數“

    -XX:MaxTenuringThreshold

    ”來設定,預設是15歲。
  2. 動态對象年齡判斷

    這裡跟這個對象年齡有另外一個規則可以讓對象進入老年代,不用等待15次GC過後才可以。

    假如說目前放對象的 Survivor 區域裡,一批對象的總大小大于了這塊 Survivor 區域的記憶體大小的50%,那麼此時大于等于這批對象年齡的對象,就可以直接進入老年代了。

    實際這個規則運作的時候是如下的邏輯:年齡1+年齡2+年齡n的多個年齡對象總和超過了 Survivor 區 域的50%,此時就會把年齡n及以上的對象都放入老年代。

  3. 大對象直接進入老年代

    有一個JVM參數,就是“

    -XX:PretenureSizeThreshold

    ”,可以把他的值設定為位元組數,比如“1048576”位元組,就是1MB。

他的意思就是,如果你要建立一個大于這個大小的對象,比如一個超大的數組,或者是别的啥東西,此時就直接把這個大對象放到老年代裡去。壓根兒不會經過新生代。

之是以這麼做,就是要避免新生代裡出現那種大對象,然後屢次躲過GC,還得把他在兩個Survivor區域裡來回複制多次之後才能進入老年代,這是很耗費時間的。

2.2 Minor GC後的對象太多無法放入Survivor區怎麼辦?

如果在Minor GC之後發現剩餘的存活對象太多了,沒辦法放入另外一塊Survivor區,

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

這個時候就必須得把這些對象直接轉移到老年代去,如下圖所示。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

2.3 老年代空間配置設定擔保規則

首先,在執行任何一次Minor GC之前,JVM會先檢查一下老年代可用的可用記憶體空間,是否大于新生代所有對象的總大小。

為啥檢查這個呢?因為最極端的情況下,可能新生代Minor GC過後,所有對象都存活下來了,新生代所有對象全部要進入老年代。

如果說發現老年代的記憶體大小是大于新生代所有對象的,此時就可以放心大膽的對新生代發起一次Minor GC了,因為即使Minor GC之 後所有對象都存活,Survivor區放不下了,也可以轉移到老年代去。

但是假如執行Minor GC之前,發現老年代的可用記憶體已經小于了新生代的全部對象大小了,那麼這個時候是不是有可能在Minor GC之後新生代的對象全部存活下來,然後全部需要轉移到老年代去,但是老年代空間又不夠?

是以假如Minor GC之前,發現老年代的可用記憶體已經小于了新生代的全部對象大小了,就會看一個“

-XX:HandlePromotionFailure

”的參數是否設定了,如果有這個參數,那麼就會繼續嘗試進行下一步判斷。

下一步判斷,就是看看老年代的記憶體大小,是否大于之前每一次Minor GC後進入老年代的對象的平均大小。

舉個例子,之前每次Minor GC後,平均都有10MB左右的對象會進入老年代,那麼此時老年代可用記憶體大于10MB。這就說明,很可能這次Minor GC過後也是差不多10MB左右的對象會進入老年代,此時老年代空間是夠的,看下圖。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

如果上面那個步驟判斷失敗了,或者是“

-XX:-HandlePromotionFailure

”參數沒設定,此時就會直接觸發一次“Full GC”,就是對老年代進行垃圾回收,盡量騰出來一些記憶體空間,然後再執行Minor GC。

如果上面兩個步驟都判斷成功了,那麼就是說可以冒點風險嘗試一下Minor GC。此時進行Minor GC有幾種可能。

第一種可能,Minor GC過後,剩餘的存活對象的大小,是小于Survivor區的大小的,那麼此時存活對象進入Survivor 區域即可。

第二種可能,Minor GC過後,剩餘的存活對象的大小,是大于 Survivor區域的大小,但是是小于老年代可用記憶體大小 的,此時就直接進入老年代即可。

第三種可能,很不幸,Minor GC過後,剩餘的存活對象的大小,大于了Survivor區域的大小,也大于了老年代可用内 存的大小。此時老年代都放不下這些存活對象了,就會發生“Handle Promotion Failure”的情況,這個時候就會觸 發一次“Full GC”。

Full GC就是對老年代進行垃圾回收,同時也一般會對新生代進行垃圾回收。

因為這個時候必須得把老年代裡的沒人引用的對象給回收掉,然後才可能讓Minor GC過後剩餘的存活對象進入老年代 裡面。

如果要是Full GC過後,老年代還是沒有足夠的空間存放Minor GC過後的剩餘存活對象,那麼此時就會導緻所謂的 “OOM”記憶體溢出了

因為記憶體實在是不夠了,你還是要不停的往裡面放對象,當然就崩潰了。

2.4 老年代垃圾回收觸發時機

簡單來說,一句話總結,對老年代觸發垃圾回收的時機,一般就是兩個:

要不然是在Minor GC之前,一通檢查發現很可能Minor GC之後要進入老年代的對象太多了,老年代放不下,此時需要提前觸發Full GC然後再帶着進行Minor GC;

要不然是在Minor GC之後,發現剩餘對象太多放入老年代都放不下了。

2.5 老年代垃圾回收算法:标記整理算法

首先标記出來老年代目前存活的對象,這些對象可能是東一個西一個的。接着會讓這些存活對象在記憶體裡進行移動,把存活對象盡量都挪動到一邊去,讓存活對象緊湊的靠在一起,避免垃圾 回收過後出現過多的記憶體碎片,然後再一次性把垃圾對象都回收掉。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

老年代的垃圾回收算法的速度至少比新生代的垃圾回收算法的速度慢10倍。

如果系統頻繁出現老年代的Full GC垃圾回收,會導緻系統性能被嚴重影響,出現頻繁卡頓的情況。

JVM優化:

所謂JVM優化,就是盡可能讓對象都在 新生代裡配置設定和回收,盡量别讓太多對象頻繁進入老年代,避免頻繁對老年代進行垃圾回收,同時給系統充足的記憶體大小,避免新生代頻繁的進行垃圾回收。

一個日處理上億資料的計算系統的JVM 優化分析過程

捋清楚。

三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?

  1. Serial和Serial Old垃圾回收器: 分别用來回收新生代和老年代的垃圾對象

    工作原理就是單線程運作,垃圾回收的時候會停止我們自己寫的系統的其他工作線程,讓我們系統直接卡死不動,然 後讓他們垃圾回收,這個現在一般寫背景Java系統幾乎不用。

  2. ParNew和CMS垃圾回收器:ParNew現在一般都是用在新生代的垃圾回收器,CMS是用在老年代的垃圾回收器,他 們都是多線程并發的機制,性能更好,現在一般是線上生産系統的标配組合。
  3. G1垃圾回收器:統一收集新生代 和老年代,采用了更加優秀的算法和設計機制。

JVM的痛點:Stop the World

在垃圾回收的這個過程

因為在垃圾回收的時候,盡可能要讓垃圾回收器專心緻志的幹工作,不能随便讓我們寫的Java系統繼續對象了,是以此時JVM會在背景 直接進入“Stop the World”狀态。

也就是說,他會直接停止我們寫的Java系統的所有工作線程,讓我們寫的代碼不再運作!

然後讓垃圾回收線程可以專心緻志的進行垃圾回收的工作,如下圖所示。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

一旦垃圾回收完畢,就可以繼續恢複我們寫的Java系統的工作線程的運作了,然後我們的那些代碼就可以繼續運作,繼續在Eden 中建立新的對象。

3.1 最常用的新生代垃圾回收器:ParNew

一般來說,在沒有最新的G1垃圾回收器之前,通常大家線上系統都是ParNew垃圾回收器作為新生 代的垃圾回收器,當然現在即使有了G1,其實很多線上系統還是用的ParNew。

通常運作在伺服器上的Java系統,都可以充分利用伺服器的多核CPU的優勢,假設伺服器 是4核CPU,如果對新生代垃圾回收的時候,理論上4核CPU就可以支援4個垃圾回收線程并行執行,可以提升4倍的性 能!如果僅僅使用單線程進行垃圾回收,會導緻沒法充分利用CPU資源。

新生代的ParNew垃圾回收器主打的就是多線程垃圾回收機制,另外一種Serial垃圾回收器主打的是單線程垃圾回收,他們倆都是回收新生代的,唯一的差別就是單線程和多線程的差別,但是垃圾回收算法是完全一樣的。

ParNew垃圾回收器如果一旦在合适的時機執行Minor GC的時候,就會把系統程式的工作線程全部停掉,禁止程式繼續運作建立新的對象,然後自己就用多個垃圾回收線程去進行垃圾回收。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

如何為線上系統指定使用ParNew垃圾回收器?

使用“

-XX:+UseParNewGC

”選項,JVM啟動之後對新生代進行垃圾回收的,就是 ParNew垃圾回收器了。

ParNew垃圾回收器預設情況下的線程數量

ParNew垃圾回收器預設給自己設定的垃圾回收線程的數量就是跟CPU的核數是一樣的。一般不用我們手動去調節,因為跟CPU核數一樣的線程數量,是可以充分進行并行處理的。

如果一定要自己調節ParNew的垃圾回收線程數量,也是可以的,使用“

-XX:ParallelGCThreads

”參數即可, 通過他可以設定線程的數量

但是建議一般不要随意動這個參數。

思考題:到底是用單線程垃圾回收好,還是多線程垃圾回收好? 到底是Serial垃圾回收器好還是ParNew垃圾回收器好?

啟動系統的時候是可以區分伺服器模式和用戶端模式的,如果你啟動系統的時候加入“-server”就是伺服器模式,如 果加入“-cilent”就是用戶端模式。他們倆的差別就是,如果你的系統部署在比如4核8G的Linux伺服器上,那麼就應該用伺服器模式,如果你的系統是運 行在比如Windows上的用戶端程式,那麼就應該是用戶端模式。

伺服器模式通常運作我們的網站系統、電商系統、業務系統、APP背景系統之類的大型系統,一般都是多核CPU,是以此時如果要垃圾回收,用ParNew更好,因為多線程并行垃圾回收,充分利用多核CPU資源,可以提升性能。如果此時用了單線程垃圾回收,那麼就有一些CPU是被浪費了,根本沒用上。

如果你的Java程式是一個用戶端程式,比如類似百度雲網盤的Windows用戶端,或者是印象筆記的Windows客 戶端,運作在Windows個人作業系統上,這種作業系統很多都是單核CPU,此時你如果要是還是用ParNew來進行垃圾回收,就會導緻一個CPU運作多個線程, 反而加重了性能開銷,可能效率還不如單線程好。

因為單CPU運作多線程會導緻頻繁的線上上下文切換,有效率開銷,建議采用Serial垃圾回收器,單CPU單線程垃圾回收即可, 反而效率更高。

3.2 老年代常用的垃圾回收器:CMS

CMS采用的是标記清理算法,其實就是我們之前給大家講過的一個算法,先通過追蹤GC Roots的方法,看看各個對象是否被GC Roots給引用了,如果是的話,那就是存活對象,否則就是垃圾對象。先将垃圾對象都标記出來,然後一次性把垃圾對象都回收掉。

這種方法最大的問題,就是會造成很多記憶體碎片。

3.2.1 CMS如何實作系統一邊工作的同時進行垃圾回收?

如果停止一切工作線程“Stop the World”,然後慢慢的去執行“标記-清理”算法,會導緻系統卡死時間過長,很多響應無法處理。

是以CMS垃圾回收器采取的是垃圾回收線程和系統工作線程盡量同時執行的模式來處理的。

CMS在執行一次垃圾回收的過程一共分為4個階段:

初始标記

并發标記

重新标記

并發清理

  1. 初始标記 :首先,CMS要進行垃圾回收時,會先執行初始标記階段,這個階段會讓系統的工作線程全部停止,進入“Stop the World”狀态,标記出來所有GC Roots 直接引用的對象。方法的局部變量和類的靜态變量是GC Roots。但是類的執行個體變量不是GC Roots。
  2. 并發标記:這個階段會讓系統線程可以随意建立各種新對象,繼續運作。

    在運作期間可能會建立新的存活對象,也可能會讓部分存活對象失去引用,變成垃圾對象。在這個過程中,垃圾回收線程,會盡可能的對已有的對象進行GC Roots追蹤。這個過程中,在進行并發标記的時候,系統程式會不停的工作,他可能會各種建立出來新的對象,部分對象可能成為垃圾。對老年代所有對象進行GC Roots追蹤,其實是最耗時的,需要追蹤所有對象是否從根源上被GC Roots引用了,但是這個最耗時的階段,是跟系統程式并發運作的,是以其實這個階段不會對 系統運作造成影響的

  3. 重新标記階段:讓系統程式停下來,再次進入“Stop the World”階段。

    然後重新标記下在第二階段裡新建立的一些對象,還有一些已有對象可能失去引用變成垃圾的情況。

  4. 并發清理:重新恢複系統程式的運作,清理掉之前标記為垃圾的對象即可。這個階段其實是很耗時的,因為需要進行對象的清理,但是他也是跟系統程式并發運作的,是以其實也不影響系統程式的執行。

3.2.2 CMS垃圾回收的細節問題

  1. 消耗CPU資源

    CMS垃圾回收器雖然能在垃圾回收的同時讓系統同時工作,但是在并發标記和并發清理兩個耗時的階段,垃圾回收線程和系統工作線程同時工作,會導緻有限的CPU資源被垃圾回收線程占用了一部分。

  2. Concurrent Mode Failure問題

    在并發清理階段,CMS隻不過是回收之前标記好的垃圾對象

    但是這個階段系統一直在運作,可能會随着系統運作讓一些對象進入老年代,同時還變成垃圾對象,這種垃圾對象是“浮動垃圾”。因為他雖然成為了垃圾,但是CMS隻能回收之前标記出來的垃圾對象,不會回收他們,需要等到下一次GC的時候才會回收他們。

是以為了保證在CMS垃圾回收期間,還有一定的記憶體空間讓一些對象可以進入老年代,一般會預留一些空間。“

-XX:CMSInitiatingOccupancyFaction

”參數可以用來設定老年代占用多少比例的時候觸發CMS垃圾回收,JDK 1.6裡面預設的值是 92%。

也就是說,老年代占用了92%空間了,就自動進行CMS垃圾回收,預留8%的空間給并發回收期間,系統程式把一些新對象放入老年代中。

如果CMS垃圾回收期間,系統程式要放入老年代的對象大于了可用記憶體空間,會發生Concurrent Mode Failure,就是說并發垃圾回收失敗,此時就會自動用“Serial Old”垃圾回收器替代CMS,就是直接強行把系統程式“Stop the World”,重新進行長時間的GC Roots追 蹤,标記出來全部垃圾對象,不允許新的對象産生

然後一次性把垃圾對象都回收掉,完事兒了再恢複系統線程。

  1. 記憶體碎片問題

    老年代的CMS采用“标記-清理”算法,每次都是标記出來垃圾對象,然後一次性回收掉,這樣會導緻大量的記憶體碎片産生。

    如果記憶體碎片太多,會導緻後續對象進入老年代找不到可用的連續記憶體空間了,太多的記憶體碎片實際上會導緻更加頻繁的Full GC。

CMS有一個參數是“

-XX:+UseCMSCompactAtFullCollection

”,預設就打開了,他意思是在Full GC之後要再次進行“Stop the World”,停止工作線程,然後進行碎片整理,就是把存活對象挪到一起,空出來大片 連續記憶體空間,避免記憶體碎片。

還有一個參數是“

-XX:CMSFullGCsBeforeCompaction

”,這個意思是執行多少次Full GC之後再執行一次記憶體碎片整理的工作,預設是0,意思就是每次Full GC之後都會進行一次記憶體整理。記憶體碎片整理完之後,存活對象都放在一起,然後空出來大片連續記憶體空間可供使用。

思考題:為什麼老年代的垃圾回收速度會比新生代的垃圾回收速度慢很多倍?到 底慢在哪裡?

新生代執行速度其實很快,因為直接從GC Roots出發就追蹤哪些對象是活的就行了,新生代存活對象是很少的,這個速度是極快的, 不需要追蹤多少對象。

然後直接把存活對象放入Survivor中,就一次性直接回收Eden和之前使用的Survivor了。

CMS的Full GC,在并發标記階段,他需要去追蹤所有存活對象,老年代存活對象很多,這個過程就會很慢;

其次并發清理階段,他不是一次性回收一大片記憶體,而是找到零零散散在各個地方的垃圾對象,速度也很慢;

然後,還得執行一次記憶體碎片整理,把大量的存活對象給挪在一起,空出來連續記憶體空間,這個過程還得“Stop the World”,那就更慢了。

萬一并發清理期間,剩餘記憶體空間不足以存放要進入老年代的對象了,引發了“Concurrent Mode Failure”問題,那更是麻煩,還得 立馬用“Serial Old”垃圾回收器,“Stop the World”之後慢慢重新來一遍回收的過程,這更是耗時了。

案例優化分析:

1.一個日處理上億資料的計算系統的JVM優化案例分析。

2. 每日上億請求量的電商系統的優化。

面試題:ParNew + CMS 的GC ,如何保證隻做young GC,JVM 參數如何配置?

首先上線系統之後,要借助一些工具觀察每秒鐘會新增多少對象在新生代裡,然後多長時間觸發一次Minor GC,平均每 次MInor GC之後會有多少對象存活,Survivor區是否可以放的下。

這裡的關鍵點就是必須讓Survivor區放下,而且不能因為動态年齡判定規則直接升入老年代。然後隻要Survivor區可以放下,那麼下次 Minor GC後還是存活這麼多對象,依然可以在另外一塊Survivor區放下,基本就不會有對象升入老年代裡去。

四、 G1 垃圾回收器

4.1 ParNew + CMS帶給我們的痛點是什麼?

無論是新生代垃圾回收,還是老年代垃圾回收,都會或多或少産生“Stop the World”現象,對系統的運作是有一定影響的。

是以其實之後對垃圾回收器的優化,都是朝着減少“Stop the World”的目标去做的。

在這個基礎之上,G1垃圾回收器就應運而生了,他可以提供比“ParNew + CMS”組合更好的垃圾回收的性能。

4.2 G1垃圾回收器

G1垃圾回收器是可以同時回收新生代和老年代的對象的,不需要兩個垃圾回收器配合起來運作,G1 最大的一個特點,就是把Java堆記憶體拆分為多個大小相等的 Region。然後G1也會有新生代和老年代的概念,但是隻不過是邏輯上的概念

也就是說,新生代可能包含了某些Region,老年代可能包含了某些Reigon,如下圖。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

G1最大的一個特點,就是可以讓我們設定一個垃圾回收的預期停頓時間

4.3 G1是如何做到對垃圾回收導緻的系統停頓可控的?

G1如果要做到這一點,他就必須要追蹤每個Region裡的回收價值。他必須搞清楚每個Region裡的對象有多少是垃圾,如果對這個Region進行垃圾回收,需要耗費多長時間,可以回收掉多少垃圾?

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

簡單來說,G1可以做到讓你來設定垃圾回收對系統的影響,他自己通過把記憶體拆分為大量小Region,以及追蹤每個Region中可以 回收的對象大小和預估時間,最後在垃圾回收的時候,盡量把垃圾回收對系統造成的影響控制在你指定的時間範圍内,同時在有限的時 間内盡量回收盡可能多的垃圾對象。

這就是G1的核心設計思路。

在G1中雖然把記憶體劃分為了很多的 Region,但是其實還是有新生代、老年代的區分。而且新生代裡還是有Eden和Survivor的劃分的。他們會各自占據不同的Region。隻不過随着對象不停的在新生代裡配置設定,屬于新生代的Region會不斷增加,Eden 和 Survivor 對應的Region也會不斷增加。

實際上新生代和老年代各自的記憶體區域是不停的變動的,由G1自動控制。

4.4 如何設定G1對應的記憶體大小?

G1對應的是一大堆的Region記憶體區域,每個Region的大小是一緻的。

預設情況下自動計算和設定的,我們可以給整個堆記憶體設定一個大小,比如說用“

-Xms

”和“

-Xmx

”來設定堆記憶體的大小。然後JVM啟動的時候一旦發現你使用的是G1垃圾回收器,可以使用“

-XX:+UseG1GC

”來指定使用G1垃圾回收器,此時會自動用堆大小除以2048。

因為JVM最多可以有2048個Region,然後Region的大小必須是2的倍數,比如說1MB、2MB、4MB之類的。

比如說堆大小是4G,那麼就是4096MB,此時除以2048個 Region ,每個 Region 的大小就是2MB。大概就是這樣子來決定Region的數量和大小的,一般保持預設的計算方式就可以。

如果通過手動方式來指定,則是“

-XX:G1HeapRegionSize

”。

剛開始的時候,預設新生代對堆記憶體的占比是5%,也就是占據200MB左右的記憶體,對應大概是100個Region,這個是可以通過“

XX:G1NewSizePercent

”來設定新生代初始占比的,其實維持這個預設值即可。

因為在系統運作中,JVM其實會不停的給新生代增加更多的Region,但是最多新生代的占比不會超過60%,可以通過“

XX:G1MaxNewSizePercent

”。

而且一旦Region進行了垃圾回收,此時新生代的 Region 數量還會減少,這些其實都是動态的。

剛開始就是一部分的Region是屬于新生代的。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

4.5 G1的新生代垃圾回收

随着不停的在新生代的Eden對應的Region中放對象,JVM就會不停的給新生代加入更多的Region,直到新生代占據堆大小的最大比例 60%。

一旦新生代達到了設定的占據堆記憶體的最大大小60%,比如都有1200個Region了,裡面的Eden可能占據了1000個Region,每個 Survivor是100個Region,而且Eden區還占滿了對象,此時如下圖所示。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

這個時候還是會觸發新生代的GC,G1就會用之前說過的複制算法來進行垃圾回收,進入一個“Stop the World”狀态

然後把Eden對應的Region中的存活對象放入S1對應的Region中,接着回收掉Eden對應的Region中的垃圾對象。

但是這個過程跟之前是有差別的,因為G1是可以設定目标GC停頓時間的,也就是G1執行GC的時候最多可以讓系統停頓多長時間,可以通過“

-XX:MaxGCPauseMills

”參數來設定,預設值是200ms。

那麼G1就會通過之前說的,對每個Region追蹤回收他需要多少時間,可以回收多少對象來選擇回收一部分的Region,保證GC停頓時間控制在指定範圍内,盡可能多的回收掉一些對象。

4.6 對象什麼時候進入老年代?

跟原來一樣,滿足以下條件:

(1)對象在新生代躲過了很多次的垃圾回收,達到了一定的年齡了,“-XX:MaxTenuringThreshold”參數可以設定這個年齡,他就 會進入老年代

(2)動态年齡判定規則,如果一旦發現某次新生代GC過後,存活對象超過了Survivor的50%

4.7 大對象Region

和之前的規則不一樣,G1提供了專門的Region來存放大對象,而不是讓大對象進入老年代的Region中。

在G1中,大對象的判定規則就是一個大對象超過了一個Region大小的50%,比如按照上面算的,每個Region是2MB,隻要一個大對 象超過了1MB,就會被放入大對象專門的Region中

而且一個大對象如果太大,可能會橫跨多個Region來存放。

【JVM 學習筆記 03】:垃圾回收算法和垃圾回收器一、新生代的垃圾回收算法:複制算法二、老年代的垃圾回收算法三、JVM中都有哪些常見的垃圾回收器,各自的特點是什麼?四、 G1 垃圾回收器五、相關名詞解釋

大對象 Region 觸發垃圾回收的時機:新生代、老年代在回收的時候,會順帶帶着大對象Region一起回收,是以這就是在G1記憶體模型下對大對象的配置設定和回收的政策。

4.8 新生代+老年代的混合垃圾回收的觸發時機?

G1有一個參數,是“

-XX:InitiatingHeapOccupancyPercent

”,他的預設值是45%

意思就是說,如果老年代占據了堆記憶體的45%的Region的時候,此時就會嘗試觸發一個新生代+老年代一起回收的混合回收階段。

此時垃圾回收不僅僅是回收老年代,還會回收新生代,還會回收大對象。,因為我們設定了對GC停頓時間的目标,是以會從新生代、老年代、大對象裡各自挑選一些Region,保證用指定 的時間(比如200ms)回收盡可能多的垃圾,這就是所謂的混合回收.

4.9 G1垃圾回收的過程

  1. 首先會觸發一個“初始标記”的操作,這個過程是需要進入“Stop the World”的,僅僅隻是标記一下GC Roots直接能引用的對象, 這個過程速度是很快的。
  2. 接着會進入“并發标記”的階段,這個階段會允許系統程式的運作,同時進行GC Roots追蹤,從GC Roots開始追蹤所有的存活對象。

    這個并發标記階段還是很耗時的,因為要追蹤全部的存活對象。但是這個階段是可以跟系統程式并發運作的,是以對系統程式的影響不太大。而且JVM會對并發标記階段對對象做出的一些修改記錄起來,比如說哪個對象被建立了,哪個對象失去了引用。

  3. 最終标記階段,這個階段會進入“Stop the World”,系統程式是禁止運作的,但是會根據并發标記 階段記錄的 那些對象修改,最終标記一下有哪些存活對象,有哪些是垃圾對象.
  4. “混合回收“階段,這個階段會計算老年代中每個Region中的存活對象數量,存活對象的占比,還有執行垃圾回收的預期性能和效率。

    接着會停止系統程式,然後全力以赴盡快進行垃圾回收,此時會選擇部分Region進行回收,因為必須讓垃圾回收的停頓時間控制在我 們指定的範圍内。

4.10 G1垃圾回收器的一些參數

-XX:G1MixedGCCountTarget

,在一次混合回收的過程中,最後一個階段執行幾次混合 回收,預設值是8次

意味着最後一個階段,先停止系統運作,混合回收一些Region,再恢複系統運作,接着再次禁止系統運作,混合回收一些Region,反 複8次。

反複回收多次,是停止系統一會兒,回收掉一些Region,再讓系統運作一會兒,然後再次停止系統一會兒,再次回收掉一些Region,這樣可以盡可能讓系統不要停頓時間過長,可以在多次回收的間隙,也運作一下。

-XX:G1HeapWastePercent

,預設值是5% 。在混合回收的時候,對Region回收都是基于複制算法進行的,都是把要回收的Region裡的存活對象放入其他 Region,然後這個Region中的垃圾對象全部清理掉,在回收過程就會不斷空出來新的Region,一旦空閑出來的Region數量達到了堆記憶體的5%,此時就會立即停止混合回收,意味着本次混合回收就結束了。

-XX:G1MixedGCLiveThresholdPercent

”,他的預設值是85%,意思就是确定要回收的Region的時候,必須是存活對象低于85%的Region才可以進行回收。

4.11 回收失敗時的Full GC

如果在進行Mixed回收的時候,無論是年輕代還是老年代都基于複制算法進行回收,都要把各個Region的存活對象拷貝到别的Region 裡去

此時萬一出現拷貝的過程中發現沒有空閑Region可以承載自己的存活對象了,就會觸發 一次失敗。

一旦失敗,立馬就會切換為停止系統程式,然後采用單線程進行标記、清理和壓縮整理,空閑出來一批Region,這個過程是極慢極慢的。

G1天生适合大記憶體機器的JVM運作,可以完美解決大記憶體垃圾回收時間過長的問題。

4.12 百萬級使用者的線上教育平台,如何基于G1垃圾優化垃圾回收性能?

案例分析和總結。

五、相關名詞解釋

(1)Minor GC / Young GC

Minor GC / Young GC, 這兩個名詞是等價的。在年輕代中的Eden記憶體區域被占滿之後,實際上就需要觸發年輕代的gc,或者是新生代的 gc。此時這個新生代gc,其實就是所謂的“Minor GC”,也可以稱之為“Young GC”,這兩個名詞,說白了,就專門針對新生代的gc。

(2)Full GC?Old GC?

其實所謂的老年代gc,稱之為“Old GC”更加合适一些,因為從字面意義上就可以了解,這就是所謂的老年代gc。

但是在這裡之是以我們把老年代GC稱之為Full GC,其實也是可以的,隻不過是一個字面意思的多種不同的說法。

為了更加精準的表述這個老年代gc的含義,一律把老年代gc稱之為Old GC,後續也如此定義。

是以在這裡,大家務必捋清這個概念,在跟面試官聊的時候,如果說到所謂的老年代gc,為了避免歧義,建議用 Old GC來指代。

(3)Full GC

對于Full GC,其實這裡有一個更加合适的說法,就是說Full GC指的是針對新生代、老年代、永久代的全體記憶體空間的垃圾回收,是以稱之為Full GC。

從字面意思上也可以了解,“Full”就是整體的意思,是以就是對JVM進行一次整體的垃圾回收,把各個記憶體區域的垃 圾都回收掉。

但是如果從字面意義上來了解,建議大家日後在外面跟别人交談的時候,把Full GC了解為針對JVM内所有記憶體區域的 一次整體垃圾回收。

(4)Major GC

還有一個名詞是所謂的Major GC,這個其實一般用的比較少,他也是一個非常容易混淆的概念

有些人把Major GC跟Old GC等價起來,認為他就是針對老年代的GC,也有人把Major GC和Full GC等價起來,認為 他是針對JVM全體記憶體區域的GC。

是以針對這個容易混淆的概念。如果聽到有人說這個Major GC的概念,可以問清楚,他到底 是想說Old GC呢?還是Full GC呢?

(5)Mixed GC

Mixed GC是G1中特有的概念,其實說白了,主要就是說在G1中,一旦老年代占據堆記憶體的45%了,就要觸發Mixed GC,此時對年輕代和老年代都會進行回收。這個概念很好了解,隻要知道是G1中特有的名詞即可。

面試題:ParNew+CMS的GC,如何保證隻做YoungGC,JVM參數如何配置?

答:需要深度結合線上系統的實際運作來看。

首先上線系統之後,要借助一些工具觀察每秒鐘會新增多少對象在新生代裡,然後多長時間觸發一次Minor GC,平均每次MInor GC之後會有多少對象存活,Survivor區是否可以放的下。

這裡的關鍵點就是必須讓Survivor區放下,而且不能因為動态年齡判定規則直接升入老年代。然後隻要Survivor區可以放下,那麼下次 Minor GC後還是存活這麼多對象,依然可以在另外一塊Survivor區放下,基本就不會有對象升入老年代裡去。