JVM垃圾回收算法與垃圾收集器

 

 

JVM是經過分代收集理論進行垃圾回收的,即新生代和老年代選擇的垃圾回收算法是不一樣的:java

  • 新生代:標記-複製算法
  • 老年代:標記-清除、標記-整理算法等。

下面來看每一個算法的理論和應用:
                
        算法

1. JVM垃圾回收算法

在這裏插入圖片描述

分代收集理論

        當前虛擬機的垃圾收集都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不一樣將內存分爲幾塊。通常將java堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。這就是分代收集理論:安全

  • 在新生代中,每次收集都會有大量對象(近99%)死去,因此能夠選擇複製算法,只須要付出少許對象的複製成本就能夠完成每次垃圾收集。
  • 而老年代的對象存活概率是比較高的,並且沒有額外的空間對它進行分配擔保,因此咱們必須選擇「標記-清除」或「標記-整理」算法進行垃圾收集。注意,「標記-清除」或「標記-整理」算法會比複製算法慢10倍以上。

爲何要分代收集:服務器

由於對象的存活週期不同,因此使用分代收集,不一樣的代收集不一樣存活週期的對象!
        多線程

①:複製算法

        「複製」(Copying)的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。由於會複製並清理已使用的通常內存,因此也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效,但卻要犧牲通常的內存空間。併發

        標記-複製 算法通常用在新生代,由於標記-複製算法只使用一半的內存空間,由於新生代對象朝生夕死的緣故,只須要付出少許的複製成本就能夠完成垃圾收集。而老年代對象存活概率高,複製的成本很大,並且內存只能使用通常,因此不適用於老年代。jvm

如圖所示:
在這裏插入圖片描述ide

        

②:標記-清除算法

算法分爲 「標記「 和 「清除」 兩個階段。標記存活的對象,清除未被標記的對象。高併發

標記-清除算法帶來的兩個問題:oop

  • ①:效率問題(若是被標記的對象太多,效率不高)
  • ②:空間問題(標記清除後會有大量的內存碎片)

內存碎片的危害是什麼?

        空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
在這裏插入圖片描述

③:標記-整理算法

        因爲複製算法不適用於老年代,根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法。該算法是在標記-清除的基礎上,增長了整理的操做,把碎片化的空間整理爲隔離的。後續步驟不是直接對可回收對象回收,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。

這種算法克服了複製算法的空間浪費問題,同時克服了標記清除算法的內存碎片化的問題;
在這裏插入圖片描述
        

2. 垃圾收集器

        垃圾回收算法是jvm內存回收過程當中具體的、通用的方法。而垃圾收集器是jvm內存回收過程當中具體的執行者,即各類GC算法的具體實現。

        目前爲止尚未萬能的垃圾收集器,咱們只能根據具體場景來選擇合適的垃圾收集器。這也是目前垃圾收集器種類繁多的緣由!!各類垃圾收集器的組合使用以下圖:

在這裏插入圖片描述
Epsilon、Shenandoah:這兩個收集器是redHat開發的,其中ShenandoahG1的加強版本,因爲他們不是Oracle公司開發的,且使用的極少,本文暫不介紹!

        

①:Serial 收集器

JVM參數設置: -XX:+UseSerialGC -XX:+UseSerialOldGC

單線程收集器,他不只只有一條GC線程,在GC時還必須中止其餘全部的工做線程(STW),不多使用。

注意:

  • ①:雖然是單線程,可是效率低但簡單而高效,相比於其餘線程,沒有線程上下文切換的開銷!
  • ②:Serial收集器的新生代採用複製算法,老年代採用標記-整理算法。
    在這裏插入圖片描述

②:Parallel Scavenge 收集器

JVM參數設置:-XX:+UseParallelGC(年輕代),-XX:+UseParallelOldGC(老年代)

JDK 1.8默認使用 Parallel垃圾收集器(年輕代和老年代都是),這個垃圾收集器沒法與CMS垃圾收集器配合使用!對於堆內存2-3個G的狀況,使用Parallel Scavenge收集器足夠應對!

多線程收集器,是Serial收集器的多線程版本,默認的收集線程數跟cpu核數相同,固然也能夠用參數- XX:ParallelGCThreads指定收集線程數,可是通常不推薦修改。

注意:
①:Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。GC總時間相對於CMS收集器較短!
②:Parallel Scavenge收集器新生代採用複製算法,老年代採用標記-整理算法。
在這裏插入圖片描述

③:ParNew 收集器

JVM參數設置:-XX:+UseParNewGC

        ParNew收集器主要做用和parallel收集器相似,區別主要在於ParNew收集器能夠配合CMS收集器使用。除了Serial收集器外,只有它能與CMS收集器配合工做。配合工做時,通常ParNew負責年輕代垃圾收集,CMS負責老年代垃圾收集!這種組合是不少公司都在用的一種垃圾收集組合

  • ParNew收集器新生代使用複製算法,老年代採用標記-整理算法
    在這裏插入圖片描述

④:CMS收集器(重點)

JVM參數設置:-XX:+UseConcMarkSweepGC(old)

CMS相對於parallel 收集器的區別?

  • CMS (Concurrent Mark Sweep)收集器是隻有老年代才能用的垃圾收集器!
  • CMS收集器使用的是 標記-清除 算法,parallel 收集器新生代使用 複製 算法,老年代採用 標記-整理 算法
  • 若是jvm堆內存過大(8G左右),使用parallel收集器時,GC時須要較長時間進行 標記-整理 ,在此期間,用戶線程是stw的,很大程度上下降了用戶體驗;而CMSparallel 的多線程GC過程分爲多個階段,在最耗時的標記階段使用併發標記,讓用戶線程和GC線程同時執行。因此在應對大內存的jvm時,明顯CMS收集器使得用戶體驗更好
  • 相對於Parallel收集器,CMS使用較短期的STW,換取用戶的體驗,由於他把最耗時的標記過程,改爲了GC線程和用戶線程並行,但因爲CMS拆分了GC過程,因此總體GC時間要長於Parallel,但stw時間更短。因此cms主要是提高用戶體驗的,其實gc效率不如Parallel

工做流程以下
在這裏插入圖片描述

  • ①:初始標記:暫停其餘線程(STW),只標記gc roots直接引用的對象,速度很快!由於初始標記並不標記gc root的全部引用。
  • ②:併發標記:根據上一步標記的對象,根據可達性分析算法找整個對象引用鏈,此過程比較耗時,因此採用用戶線程和GC線程併發執行,不會STW,保證了用戶體驗,這點也是cms收集器飽受青睞的緣由之一。但正由於併發標記,用戶線程也在執行,就可能會出現多標或漏標的問題。
  • ③:從新標記:從新標記階段就是爲了修正併發標記期間由於用戶程序繼續運行而致使多標或漏標的問題。這個階段主要用到三色標記的更新算法(增量更新、原始快照),速度比初始標記慢一點,但遠比並發標記時間短。
  • ④:併發清理:開啓用戶線程和GC線程併發執行,提升了速度,但同時帶來了和併發標記一樣的問題,這種問題主要經過三色標記算法來解決的(下文會有講解)
  • ⑤:併發重置:重置本次GC過程當中的標記數據。

CMS收集器的優缺點

  • 優勢:
    • ①:併發收集,低停頓,用戶體驗較好
  • 缺點:
    • ①:GC線程和用戶線程併發執行,會存在cpu上下文切換,影響GC效率;
    • ②:併發清理階段,用戶線程可能會產生新的垃圾對象。也就是說清理完成後,本來已清除乾淨的位置上仍是有用戶線程產生的新垃圾,這個垃圾被稱爲浮動垃圾。浮動垃圾不影響程序運行,本次GC過程沒法處理浮動垃圾,要等到下次GC處理。
    • ③:垃圾回收算法用的 標記-清除 ,會有空間碎片產生,可使用數- XX:+UseCMSCompactAtFullCollection可讓jvm在執行完標記清除後再作整理,整理是也會stw,但時間較短!
    • ④:在併發標記和併發清理階段,一邊GC回收,用戶程序一邊執行,若是用戶線程產生了一個大對象,會直接進入老年代,而此時的老年代尚未GC回收完畢,已經沒有足夠的內存去接收這個大對象了。 這時就會出現 "concurrent mode failure"(併發修改失敗),此時會stop the world全部用戶線程,專心作垃圾收集,可是用的是serial old串行垃圾收集器來回收,這個串行垃圾收集器效率至關低!代價比較大,儘可能避免!

CMS的相關核心參數

--xx-xx三種jvm參數前綴有什麼不一樣:x的個數越多,表明這個參數的版本支持變數越高,有可能jdk8適用,jdk9就廢除掉了!

  • -XX:+UseConcMarkSweepGC:啓用cms
  • -XX:ConcGCThreads:併發的GC線程數
  • -XX:+UseCMSCompactAtFullCollection:FullGC以後作壓縮整理(減小碎片)
  • -XX:CMSFullGCsBeforeCompaction:多少次FullGC以後壓縮一次,默認是0,表明每次FullGC後都會壓縮一次
  • -XX:CMSInitiatingOccupancyFraction: 當老年代使用達到該比例時會觸發FullGC(默認是92%這個參數能夠防止concurrent mode failure
  • -XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收百分比(-XX:CMSInitiatingOccupancyFraction設定的值),若是不配置此參數,-XX:CMSInitiatingOccupancyFraction設定的值無效!由於jvm默認會根據gc狀況動態調整回收的百分比,相似於元空間的自動擴容、縮容!
  • -XX:+CMSScavengeBeforeRemark:在CMS GC前啓動一次minor gc,下降CMS GC標記階段(也會對年輕代一塊兒作標記,若是在minor gc就幹掉了不少對垃圾對象,標記階段就會減小一些標記時間)時的開銷,通常CMS的GC耗時 80%都在標記階段
  • -XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執行,縮短STW
  • -XX:+CMSParallelRemarkEnabled:在從新標記的時候多線程執行,縮短STW;

問題一:「concurrent mode failure」(併發修改失敗)怎麼預防?

        因爲默認老年代空間達到92% 就會full GC,固然這個值是能夠經過參數調的。在併發標記或併發清理階段,若是不斷有大對象進入老年代,老年代剩餘的8%空間很快會被填滿,此時就會出現"concurrent mode failure"。咱們能夠經過 -XX:CMSInitiatingOccupancyFraction=80 參數來調整老年代的full GC發生時機爲80%,讓老年代發生GC時還有更多空間存儲新生代存活的大對象!

        
問題二:"Parallel 和CMS收集器使用場景

        JDK8默認的垃圾回收器是-XX:+UseParallelGC(年輕代)和-XX:+UseParallelOldGC(老年代)。
若是內存較大(超過4個G,8個G之內,只是經驗值),系統對停頓時間比較敏感,咱們可使用ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)這兩個垃圾收集器配合使用!

        

三色標記算法解決漏標的原理

        在併發標記的過程當中,由於標記期間應用線程還在繼續跑,對象間的引用可能發生變化,多標和漏標的狀況就有可能發生,可使用三色標記來解決。

三色標記原理

三色標記把可達性分析遍歷對象過程當中遇到的對象, 按照「是否訪問過」這個條件標記成如下三種顏色:

  • ①:黑色: 表示對象已經被垃圾收集器訪問過, 且這個對象的全部引用都已經掃描過。 黑色的對象表明已經掃描 過, 它是安全存活的, 若是有其餘對象引用指向了黑色對象, 無須從新掃描一遍。 黑色對象不可能直接(不通過 灰色對象) 指向某個白色對象。
  • ②:灰色: 表示對象已經被垃圾收集器訪問過, 但這個對象上至少存在一個引用尚未被掃描過。
  • ③:白色: 表示對象還沒有被垃圾收集器訪問過。 顯然在可達性分析剛剛開始的階段, 全部的對象都是白色的, 若 在分析結束的階段, 仍然是白色的對象, 即表明不可達。

如圖所示
在這裏插入圖片描述
三色標記過程分析:假如:A類中包含了B ,B類中包含了C和D。

  • ①:可達性分析算法會先根據gc roots的局部變量a去找,a指向了A類,就掃描了A中的全部對象(此例中只有一個B),那麼A就會被標爲黑色。回收時不會管黑色對象,由於已經分析完了。
  • ②:而後根據可達性分析算法,開始掃描B,B中包含了C和D,若是 此時恰好掃描完C,還沒開始掃描D時。當前的B爲灰色,表明至少存在一個引用尚未被掃描過。
  • ③:因爲上一步C已經被掃描,且沒有更多引用。因此爲黑色。而D在那個時機中還沒被掃描,爲白色。

        剛開始默認都是白色對象,掃描標記完成後,黑色和灰色對象不會被回收,白色會回收。明白了三色標記原理後,來看一下具體是如何解決漏標問題的!

問題三:併發標記階段的多標和漏標怎麼解決?

  • 多標:會產生浮動垃圾。因爲併發運行的用戶線程結束,會改變某些已標記過的對象的狀態,好比gc root被銷燬,那麼會有部分GC線程已掃描過的黑色對象轉變爲白色對象,那麼本輪GC不會回收這些浮動垃圾,留着下一次GC進行回收,浮動垃圾並不影響垃圾回收的正確性。

  • 漏標:漏標會致使被引用的對象被當成垃圾誤刪除,這是嚴重bug,必須解決。產生緣由:併發執行中,用戶線程把某些白色對象的引用指向了GC已掃描過的黑色對象,那麼最初的白色對象也變成黑色對象了,而GC線程並不知道這個過程,會刪除有用的對象。

  • 漏標有兩種解決方案:

    • ①:增量更新(Incremental Update)
      • 所謂增量就是GC期間新增了對象引用。增量更新就是當黑色對象插入新的指向白色對象的引用關係時, 就將這個新插入的增量記錄下來到保存一個集合裏邊。
      • 等併發標記結束以後, 在從新標記過程當中中將這些記錄過的引用關係中的黑色對象爲根, 從新掃描一次。 從新標記期間程序是stw狀態,只有GC線程併發執行,因此不會再次產生漏標,且速度較快。
    • ②:原始快照(Snapshot At The Beginning,SATB)
      • 原始快照主要針對的是GC期間引用關係被刪除的操做。就是當灰色對象要刪除指向白色對象的引用關係時, 就將這個引用關係記錄到一個容器裏邊。
      • 等併發標記結束以後, 在從新標記過程當中把容器裏邊的白色對象直接標記爲黑色(目的就是讓這種對象在本輪gc清理中能存活下來,待下一輪gc的時候從新掃描,這個對象也有多是浮動垃圾),
    • 增量更新原始快照兩種方案的區別在於:
      • 增量更新須要在從新標記階段以黑色對象爲根,在深度掃描一次,效率可能會有所影響!
      • 原始快照在從新標記階段直接把白色對象變成黑色,不須要深度掃描,可是可能這個對象並無被引用,產生浮動垃圾!不過下次GC就會清理,不影響

寫屏障

         以上不管是增量更新仍是原始快照虛擬機的記錄操做都是經過寫屏障實現的。由於想要增長引用或者刪除引用,必有引用賦值操做這一步,寫屏障就是利用AOP的理念,在引用賦值操做先後,加入一些記錄處理,收集這些將要賦值的引用,並保存起來!

給某個對象的成員變量賦值時,其底層代碼大概長這樣:

/**
* @param field 某對象的成員變量,如 a.b.d 
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 賦值操做
}

所謂的寫屏障,其實就是指在賦值操做先後,加入一些處理(能夠參考AOP的概念):

void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field);          // 寫屏障-寫前操做
    *field = new_value; 			// 賦值操做
    post_write_barrier(field, value);  // 寫屏障-寫後操做
}
  • 寫屏障實現增量更新
    • 當對象A的成員變量的引用發生變化時,好比新增引用a.d = d,咱們能夠利用寫屏障,在增量更新以後,將A新的成員變量引用對象d記錄下來
    • remark_set.add(new_value); // 在增量更新以後,記錄新引用的對象
  • 寫屏障實現原始快照SATB
    • 當對象B的成員變量的引用發生變化時,好比引用刪除a.b.d = null,咱們能夠利用寫屏障,在引用刪除以前,將B原來成員變量的引用對象d記錄下來
    • remark_set.add(old_value); // 在引用刪除以前,記錄原來的引用對象

對於讀寫屏障,以Java HotSpot VM爲例,其併發標記時對漏標的處理方案以下:

  • CMS:寫屏障 + 增量更新
  • G1Shenandoah:寫屏障 + 原始快照SATB
  • ZGC:讀屏障

 

⑤:G1垃圾收集器

JVM參數設置:-XX:+UseG1GC

JDK 1.9默認使用 G1

        G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高機率知足GC停頓時間要求的同時,還具有高吞吐量性能特徵.G1垃圾收集器摒棄了分代收集理念,只保留了年輕代和老年代的概念,但物理上已經不存在了,而是以(能夠不連續)Region的形式來存儲對象。

Forget 分代收集:
在這裏插入圖片描述
Region的形式來存儲對象:每個小塊能夠看作是一個Region在這裏插入圖片描述

G1垃圾收集器的特色?

  • ①:可自定義stw時間
    • G1垃圾收集器主要針對大內存的機器,能夠設置GC停頓時間(默認200ms)用戶可控(經過參數"- XX:MaxGCPauseMillis"指定),以極高的機率知足GC停頓的同時,也保證了高吞吐量的特徵。G1垃圾收集器在邏輯上保留了年輕代、老年代的概念,但在物理上已經拋棄了這些,年輕代和老年代區域能夠任意轉換。
  • ②:年輕代自動擴容
    • 年輕代默認佔堆空間的5%(能夠經過-XX:G1NewSizePercent設置新生代初始佔比),在系統運行中,JVM會不停的給年輕代增長更多的Region,可是最多新生代的佔比不會超過60%(能夠經過-XX:G1MaxNewSizePercent進行調整),這也是與其餘垃圾收集器的不一樣之處!好比:堆大小爲4096M,那麼年輕代默認佔據200MB左右的內存,對應大概是100個Region,每一個Region大小爲2M
  • ③:Region存儲機制
    • G1垃圾收集器將堆分爲多個大小相等的獨立區域(Regin),jvm最多存在2048Regin,通常Region大小等於堆大小除以2048,若是堆內存大小是4096M,那每一個Region大小默認爲2M。可以使用-XX:G1HeapRegionSize手動指定Region大小。年輕代中的EdenSurvivor對應的region也跟以前同樣,默認8:1:1,假設年輕代如今有1000個region,eden區對應800個,s0對應100個,s1對應100個。一個Region可能以前是年輕代,若是Region進行了垃圾回收,以後可能又會變成老年代,也就是說Region的區域功能可能會動態變化。
  • ④:專門處理大對象的Humongous區
    • G1垃圾收集器對於對象回收規則和其餘垃圾收集器同樣,惟一不一樣的是對大對象的處理。之前的垃圾收集器會根據動態年齡判斷等把大對象放入老年代。而G1則有專門的處理大對象的Regin---->Humongous區。若是一個對象超過了一個Regin大小的50%,則會進入Humongous區,一個Humongous放不下,會橫跨多個Humongous放置這個對象!Full GC的時候除了收集年輕代和老年代以外,也會將Humongous區一併回收。
      用處:能夠節約老年代的空間,避免由於老年代空間不夠的GC開銷。
  • ⑤:採用複製算法回收垃圾
    • g1垃圾回收算法採用複製算法,將一個region中的存活對象複製到另外一個空的region中,並清空原region中的對象。由於G1中年輕代和老年代都是以region進行存儲的,因此年輕代和老年代均可以使用複製算法! 這種不會像CMS那樣回收完由於有不少內存碎片還須要整理一次,G1採用複製算法回收幾乎不會有太多內存碎片

 

G1的垃圾回收過程

G1由於在物理上已經不區分年輕代、老年代,因此邏輯上的年輕代,老年代都用的同一個垃圾收集器G1。
在這裏插入圖片描述

  • 初始標記: 同CMS的初始標記。
  • 併發標記: 同CMS的併發標記。
  • 最終標記: 同CMS的從新標記。只不過G1使用原始快照解決漏標問題,而CMS使用增量更新解決漏標問題
  • 篩選回收: 篩選回收階段和CMS不一樣,CMS中用戶線程和GC線程併發清除,不會stw;G1只有GC線程工做,此時會stw。篩選回收會首先對regin的回收成本作計算排序,再根據用戶指望的GC停頓時間(默認200ms)來制定回收計劃,根據回收計劃回收垃圾

 

G1的垃圾收集分類

  • ①:YoungGC
    • G1的eden區默認佔堆的5%YoungGC並非說Eden區滿了就馬上觸發,G1會計算如今回收Eden須要多長時間,若是時間遠小於用戶設定的指望時間(使用-XX:MaxGCPauseMills設定),就會給Eden區擴容,直到擴容後的Eden區再次放滿,再次計算。。。直到回收須要時長約等於用戶設定的指望停頓時間,此時纔會觸發YoungGC!
  • ②:MixedGC
    • MixedGC並非FullGC,MixedGC的發生條件:經過-XX:InitiatingHeapOccupancyPercent設置老年代的佔用比,默認是45%,若是達到這個比例就觸發MixedGC,會回收Young、部分Old、Humongous區的對象。好比:堆默認有2048個region,若是有接近1000個region都是老年代的region,則可能就要觸發MixedGC了,MixedGc使用複製算法。須要把各個region中存活的對象拷貝到別的region裏去,拷貝過程當中若是發現沒有足夠的空region可以承載拷貝對象就會觸發一次真正的Full GC
  • ③:FullGC
    • 中止系統程序,而後採用單線程進行標記、清理和壓縮整理,好空閒出來一批Region來供下一次MixedGC使用,這個過程是很是耗時的。(Shenandoah優化成多線程收集了)

G1收集器參數設置

  • -XX:+UseG1GC:使用G1收集器
  • -XX:ParallelGCThreads:指定GC工做的線程數量
  • -XX:G1HeapRegionSize:指定分區大小(1MB~32MB,且必須是2的N次冪),默認將整堆劃分爲2048個分區,默認2M
  • -XX:MaxGCPauseMillis:目標暫停時間(默認200ms)
  • -XX:G1NewSizePercent:新生代內存初始空間(默認整堆5%,值配置整數,默認就是百分比)
  • -XX:G1MaxNewSizePercent:新生代內存最大空間
  • -XX:TargetSurvivorRatio:Survivor區的填充容量(默認50%),其實就是以前說的動態年齡判斷。Survivor區域裏的一批對象(年齡1+年齡2+年齡n的多個年齡對象)總和超過了Survivor區域的50%,此時就會把年齡n(含)以上的對象都放入老年代
  • -XX:MaxTenuringThreshold:最大年齡閾值(默認15)
  • -XX:InitiatingHeapOccupancyPercent:老年代佔用空間達到整堆內存閾值(默認45%),則執行新生代和老年代的混合收集(MixedGC),好比咱們以前說的堆默認有2048個region,若是有接近1000個region都是老年代的region,則可能就要觸發MixedGC了
  • -XX:G1MixedGCLiveThresholdPercent:region中的存活對象低於這個值時纔會回收該region(默認85%) ,若是超過這個值,存活對象過多,回收的的意義不大。
  • -XX:G1MixedGCCountTarget:在一次回收過程當中指定作幾回篩選回收(默認8次),在最後一個篩選回收階段能夠回收一會,而後暫停回收,恢復系統運行,一會再開始回收,這樣可讓系統不至於單次停頓時間過長。這個過程至關於把篩選回收階段切分爲 GC線程 – 用戶線程 – GC線程,注意這過程不是併發,而是串行
  • -XX:G1HeapWastePercent(默認5%):gc過程當中空出來的region是否充足閾值,在混合回收的時候,對Region回收都是基於複製算法進行的,都是把要回收的Region裏的存活對象放入其餘Region,而後這個Region中的垃圾對象所有清理掉,這樣的話在回收過程就會不斷空出來新的Region,一旦空閒出來的Region數量達到了堆內存的5%,此時就會當即中止混合回收,意味着本次混合回收就結束了。

問題一:爲何g1篩選回收階段不作成和CMS用戶線程和GC線程併發呢?

        CMS用戶線程和GC線程併發的最主要做用就是防止STW的時間過長而設計。但由於g1垃圾收集器的STW時間是用戶可控的,就解決了CMS併發收集存在的問題。
在問題已解決的同時,關閉用戶線程將大幅度提升GC效率,即知足了GC停頓,還保證了GC的高吞吐量!

        
問題二:用戶能夠隨意設置stw停頓時間嗎?爲何?

  • ①:毫無疑問, 能夠由用戶指按期望的停頓時間是G1收集器很強大的一個功能, 設置不一樣的指望停頓時間, 可以使得G1在不 同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。 不過, 這裏設置的「指望值」必須是符合實際的。
  • ②:這個停頓時 間再怎麼低也得有個限度。 它默認的停頓目標爲兩百毫秒, 通常來講, 回收階段佔到幾十到一百甚至接近兩百毫秒都很 正常, 但若是咱們把停頓時間調得很是低, 譬如設置爲二十毫秒, 極可能出現的結果就是因爲停頓目標時間過短, 導 致每次選出來的回收集只佔堆內存很小的一部分, 收集器收集的速度逐漸跟不上分配器分配的速度, 致使垃圾慢慢堆 積。 極可能一開始收集器還能從空閒的堆內存中得到一些喘息的時間, 但應用運行時間一長就不行了, 最終佔滿堆引起 Full GC反而下降性能。
  • ③:因此一般把指望停頓時間設置爲一兩百毫秒或者兩三百毫秒會是比較合理的。保證年輕代gc別太頻繁的同時,還得考慮 每次年輕代gc事後的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸發mixed gc.

        
問題三:什麼場景適合使用G1收集器?

  • ①:8GB以上的堆內存 。由於G1收集器的底層算法是比CMS要複雜的。若是在低內存中使用G1,原本垃圾也不是不少,算法還要佔用必定時間。可能得不償失,因此g1要物盡其用,儘可能在大內存中使用!
  • ②:對停頓時間要求高,注重用戶體驗的場景

        好比像kafka這種支持高併發的系統,每秒處理幾萬甚至幾十萬消息時很正常的,通常來講部署kafka須要用大內存機器(好比64G),那麼年輕代就有40多個G,普通的Young GC 須要掃描40G空間花費的時間是很是多的,可能最快也要幾秒鐘。

        按kafka這個併發量,放滿三四十G的eden區可能也就一兩分鐘吧,那麼意味着整個系統每運行一兩分鐘就會由於young gc卡頓幾秒鐘無法處理新消息,顯然是不行的 ,那麼對於這種狀況如何優化呢?

        咱們可使用G1收集器,設置 -XX:MaxGCPauseMills 爲50ms,假設50ms可以回收三到四個G內存,而後50ms的卡頓其實徹底可以接受,用戶幾乎無感知,那麼整個系統就能夠在卡頓幾乎無感知的狀況下一邊處理業務一邊收集垃圾。

        G1天生就適合這種大內存機器的JVM運行,能夠比較完美的解決大內存垃圾回收時間過長的問題。

        
問題四:在併發標記產生的漏標中,爲何G1用(原始快照)SATB?CMS用增量更新?

        在解決漏標問題時,增量更新須要以黑色對象爲根,在經過gc root作一次深度掃描,這其中還可能包括跨代引用等狀況,這個過程是挺耗費時間的。而原始快照則只須要把集合中的白色對象引用置爲黑色,默認這個對象是有用的,不能被回收,即便它多是浮動垃圾。這種簡單粗暴的方式,雖然可能產生多的浮動垃圾,但不須要深度掃描。

         G1的不少對象都位於不一樣的regin中,這個regin是有不少個的,若是使用增量更新要從不少個regin中找gc root的引用關係,很是耗時。而使用原始快照不須要在從新標記階段再次深度掃描對象,只是簡單標記,等到下一輪GC 再深度掃描。因此G1使用原始快照相對於增量更新效率會高。而CMS使用增量更新,由於CMS就一塊老年代區域,深度掃描的話影響也不是很大!

 

⑥:ZGC垃圾收集器

ZGC是一款JDK 11中新加入的具備實驗性質的低延遲垃圾收集器,在目前的jdk8中並不適用!
在這裏插入圖片描述
ZGC的特色

  • ①:支持TB量級的堆內存,好像目前能支持到16TB吧,比G1更大
  • ②:GC停頓時間不超過10ms,且不隨堆內存增大而增大!由於ZGC中全部的垃圾收集階段幾乎都是併發執行!
  • ③:最壞狀況下GC吞吐量(垃圾回收總時間)不超過原時間的15%,這個就很厲害了,G一、CMS都是經過延長回收時間來增長用戶體驗的!
  • ④:ZGC完全拋棄了分帶概念,再也不分帶,由於分代實現起來麻煩,做者就先實現出一個比較簡單可用的單代版本
  • ⑤:ZGC也是基於Region來實現內存佈局的,分爲大、中、小三類
    • 小型Region(Small Region) : 容量固定爲2MB, 用於放置小於256KB的小對象。
    • 中型Region(Medium Region) : 容量固定爲32MB, 用於放置大於等於256KB但小於4MB的對象。
    • 大型Region(Large Region) : 容量不固定, 能夠動態變化, 但必須爲2MB的整數倍, 用於放置4MB或 以上的大對象。

 

ZGC的運做過程
在這裏插入圖片描述

  • ①:併發標記: 與G1同樣,併發標記是遍歷對象圖作可達性分析的階段,它的初始標記 (Mark Start)和最終標記(Mark End)也會出現短暫的停頓,與G1不一樣的是, ZGC的標記是在指針上而不是在對象頭上。該階段會更新顏色指針
  • ②:併發預備重分配: 回收的準備階段,此階段統計得出要回收那些regin,用這些refin組成重分配集(relocation set)。
  • ③:併發重分配: 把預分配算出來的重分配集的regin,複製到新的空regin上,併爲重分配集中的每個regin維護一個轉發表,記錄着舊對象到新對象的轉發關係。
    若是用戶線程此時並 發訪問了位於重分配集中的對象,此次訪問將會被預置的內存屏障(讀屏障)所截獲,而後當即根據Region上的轉發表記錄將訪問轉發到新複製的對象上,並同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行爲稱爲指 針的「自愈」(Self-Healing)能力

併發重分配過程

  • ①:先把分配集中的對象複製到新的regin中去。
  • ②:原來對象的引用並不會立刻更新,這個更新是惰性更新。
  • ③:當併發的用戶線程用到這個對象後才更新,這個過程使用讀屏障來實現。當用戶線程要用這個對象時,經過讀屏障更新這個對象的引用到新的regin中,讀屏障利用相似AOP的理論操做的。
    • 讀屏障怎麼知道新的對象地址呢?
      併發重分配在複製對象時會維護一個轉發表,經過轉發表得到!
  • ④:併發重映射: 把重分配集中的舊對象的引用指向併發重分配過程新分配的對象空間,通常在下一次gc中執行,由於本次GC,已經由併發重分配中的讀屏障處理過了。

        
問題:ZGC和G1在清理垃圾階段的區別是什麼?

        zgc和g1的最大區別是在篩選回收階段,G1是GC線程併發執行清理,此時STW,修改對象引用很方便。ZGC是GC執行清理時和用戶線程併發操做,沒有stw,複雜度很高

顏色指針

        以下圖所示,ZGC的核心設計之一。之前的垃圾回收器的GC信息都保存在對象頭中, 而ZGC的GC信息保存在指針中。

在這裏插入圖片描述
顏色指針的三大優點:

  1. 一旦某個Region的存活對象被移走以後,這個Region當即就可以被釋放和重用掉,而沒必要等待整個堆中全部指 向該Region的引用都被修正後才能清理,這使得理論上只要還有一個空閒Region,ZGC就能完成收集。
  2. 顏色指針能夠大幅減小在垃圾收集過程當中內存屏障的使用數量,ZGC只使用了讀屏障。
  3. 顏色指針具有強大的擴展性,它能夠做爲一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數 據,以便往後進一步提升性能。

 

 

3. 如何選擇垃圾收集器

  1. 優先調整堆的大小讓服務器本身來選擇
  2. 若是內存小於100M,使用串行收集器
  3. 若是是單核,而且沒有停頓時間的要求,串行或JVM本身選擇
  4. 若是容許停頓時間超過1秒,選擇並行或者JVM本身選
  5. 若是響應時間最重要,而且不能超過1秒,使用併發收集器
  6. 4G如下能夠用parallel,4-8G能夠用ParNew+CMS,8G以上能夠用G1,幾百G以上用ZGC

 

4. 安全點與安全區域

安全點

        就是指代碼中一些特定的位置,當線程運行到這些位置時它的狀態是肯定的,這樣JVM就能夠安全的進行一些操做,好比GC等,因此GC不是想何時作就當即觸發的,是須要等待全部線程運行到安全點後才能觸發。若是馬上掛起全部用戶線程,可能會破壞某些用戶線程的原子性,好比:i++、jvm底層程序計數器的跳轉等。

        大致實現思想是當垃圾收集須要中斷線程的時候, 不直接對線程操做, 僅僅簡單地設置一個標誌位, 各個線程執行過程 時會不停地主動去輪詢這個標誌, 一旦發現中斷標誌爲真時就本身在最近的安全點上主動中斷掛起。 輪詢標誌的地方和 安全點是重合的。

這些特定的安全點位置主要有如下幾種:

  1. 方法返回以前
  2. 調用某個方法以後
  3. 拋出異常的位置
  4. 循環的末尾
  5. 調用某個方法以前

        
安全區域

        若是一個線程處於 Sleep 或中斷狀態,它就不能掃描安全點,響應 JVM 的中斷請求。那麼他周圍的一片區域都是稱爲安全區域,這個區域的引用關係不會發生變化。在這個區域內的任意地方開始 GC 都是安全的。

相關文章
相關標籤/搜索