JVM是經過分代收集理論進行垃圾回收的,即新生代和老年代選擇的垃圾回收算法是不一樣的:java
下面來看每一個算法的理論和應用:
算法
當前虛擬機的垃圾收集都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不一樣將內存分爲幾塊。通常將java堆分爲新生代和老年代,這樣咱們就能夠根據各個年代的特色選擇合適的垃圾收集算法。這就是分代收集理論:安全
爲何要分代收集:服務器
由於對象的存活週期不同,因此使用分代收集,不一樣的代收集不一樣存活週期的對象!
多線程
「複製」(Copying)的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。由於會複製並清理已使用的通常內存,因此也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效,但卻要犧牲通常的內存空間。併發
標記-複製 算法通常用在新生代,由於標記-複製算法只使用一半的內存空間,由於新生代對象朝生夕死的緣故,只須要付出少許的複製成本就能夠完成垃圾收集。而老年代對象存活概率高,複製的成本很大,並且內存只能使用通常,因此不適用於老年代。jvm
如圖所示:
ide
算法分爲 「標記「 和 「清除」 兩個階段。標記存活的對象,清除未被標記的對象。高併發
標記-清除算法帶來的兩個問題:oop
內存碎片的危害是什麼?
空間碎片太多可能會致使,當程序在之後的運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
因爲複製算法不適用於老年代,根據老年代的特色,有人提出了另一種「標記-整理」(Mark-Compact)算法。該算法是在標記-清除的基礎上,增長了整理的操做,把碎片化的空間整理爲隔離的。後續步驟不是直接對可回收對象回收,而是讓全部存活的對象向一端移動,而後直接清理掉端邊界之外的內存。
這種算法克服了複製算法的空間浪費問題,同時克服了標記清除算法的內存碎片化的問題;
垃圾回收算法是jvm內存回收過程當中具體的、通用的方法。而垃圾收集器是jvm內存回收過程當中具體的執行者,即各類GC算法的具體實現。
目前爲止尚未萬能的垃圾收集器,咱們只能根據具體場景來選擇合適的垃圾收集器。這也是目前垃圾收集器種類繁多的緣由!!各類垃圾收集器的組合使用以下圖:
Epsilon、Shenandoah
:這兩個收集器是redHat
開發的,其中Shenandoah
是G1
的加強版本,因爲他們不是Oracle
公司開發的,且使用的極少,本文暫不介紹!
JVM參數設置: -XX:+UseSerialGC -XX:+UseSerialOldGC
單線程收集器,他不只只有一條GC線程,在GC時還必須中止其餘全部的工做線程(STW),不多使用。
注意:
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收集器新生代採用複製算法,老年代採用標記-整理算法。
JVM參數設置:-XX:+UseParNewGC
ParNew
收集器主要做用和parallel
收集器相似,區別主要在於ParNew
收集器能夠配合CMS
收集器使用。除了Serial
收集器外,只有它能與CMS
收集器配合工做。配合工做時,通常ParNew
負責年輕代垃圾收集,CMS
負責老年代垃圾收集!這種組合是不少公司都在用的一種垃圾收集組合
JVM參數設置:-XX:+UseConcMarkSweepGC(old)
CMS相對於parallel 收集器的區別?
CMS
(Concurrent Mark Sweep)收集器是隻有老年代才能用的垃圾收集器!CMS
收集器使用的是 標記-清除 算法,parallel
收集器新生代使用 複製 算法,老年代採用 標記-整理 算法parallel
收集器時,GC時須要較長時間進行 標記-整理 ,在此期間,用戶線程是stw
的,很大程度上下降了用戶體驗;而CMS
把parallel
的多線程GC過程分爲多個階段,在最耗時的標記階段使用併發標記,讓用戶線程和GC線程同時執行。因此在應對大內存的jvm
時,明顯CMS
收集器使得用戶體驗更好Parallel
收集器,CMS使用較短期的STW
,換取用戶的體驗,由於他把最耗時的標記過程,改爲了GC線程和用戶線程並行,但因爲CMS
拆分了GC過程,因此總體GC時間要長於Parallel
,但stw
時間更短。因此cms
主要是提高用戶體驗的,其實gc
效率不如Parallel
!工做流程以下
gc roots
直接引用的對象,速度很快!由於初始標記並不標記gc root
的全部引用。STW
,保證了用戶體驗,這點也是cms
收集器飽受青睞的緣由之一。但正由於併發標記,用戶線程也在執行,就可能會出現多標或漏標的問題。CMS收集器的優缺點
- XX:+UseCMSCompactAtFullCollection
可讓jvm在執行完標記清除後再作整理,整理是也會stw
,但時間較短!"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 root被銷燬,那麼會有部分GC線程已掃描過的黑色對象轉變爲白色對象,那麼本輪GC不會回收這些浮動垃圾,留着下一次GC進行回收,浮動垃圾並不影響垃圾回收的正確性。
漏標:漏標會致使被引用的對象被當成垃圾誤刪除,這是嚴重bug,必須解決。產生緣由:併發執行中,用戶線程把某些白色對象的引用指向了GC已掃描過的黑色對象,那麼最初的白色對象也變成黑色對象了,而GC線程並不知道這個過程,會刪除有用的對象。
漏標有兩種解決方案:
GC期間新增了對象引用
。增量更新就是當黑色對象插入新的指向白色對象的引用關係時, 就將這個新插入的增量記錄下來到保存一個集合裏邊。stw
狀態,只有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.d = d
,咱們能夠利用寫屏障,在增量更新以後,將A新的成員變量引用對象d
記錄下來remark_set.add(new_value); // 在增量更新以後,記錄新引用的對象
a.b.d = null
,咱們能夠利用寫屏障,在引用刪除以前,將B原來成員變量的引用對象d
記錄下來remark_set.add(old_value); // 在引用刪除以前,記錄原來的引用對象
對於讀寫屏障,以Java HotSpot VM
爲例,其併發標記時對漏標的處理方案以下:
JVM參數設置:-XX:+UseG1GC
JDK 1.9默認使用 G1
G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高機率知足GC停頓時間要求的同時,還具有高吞吐量性能特徵.G1
垃圾收集器摒棄了分代收集理念,只保留了年輕代和老年代的概念,但物理上已經不存在了,而是以(能夠不連續)Region
的形式來存儲對象。
Forget
分代收集:
以Region
的形式來存儲對象:每個小塊能夠看作是一個Region
!
G1垃圾收集器的特色?
200ms
)用戶可控(經過參數"- XX:MaxGCPauseMillis
"指定),以極高的機率知足GC停頓的同時,也保證了高吞吐量的特徵。G1垃圾收集器在邏輯上保留了年輕代、老年代的概念,但在物理上已經拋棄了這些,年輕代和老年代區域能夠任意轉換。5%
(能夠經過-XX:G1NewSizePercent
設置新生代初始佔比),在系統運行中,JVM會不停的給年輕代增長更多的Region
,可是最多新生代的佔比不會超過60%
(能夠經過-XX:G1MaxNewSizePercent
進行調整),這也是與其餘垃圾收集器的不一樣之處!好比:堆大小爲4096M
,那麼年輕代默認佔據200MB
左右的內存,對應大概是100
個Region,每一個Region
大小爲2MRegin
),jvm最多存在2048
個Regin
,通常Region大小等於堆大小除以2048
,若是堆內存大小是4096M
,那每一個Region
大小默認爲2M。可以使用-XX:G1HeapRegionSize
手動指定Region大小。年輕代中的Eden
和Survivor
對應的region
也跟以前同樣,默認8:1:1
,假設年輕代如今有1000個region,eden區對應800個,s0對應100個,s1對應100個。一個Region可能以前是年輕代,若是Region進行了垃圾回收,以後可能又會變成老年代,也就是說Region的區域功能可能會動態變化。Humongous
區。若是一個對象超過了一個Regin大小的50%
,則會進入Humongous
區,一個Humongous
放不下,會橫跨多個Humongous
放置這個對象!Full GC
的時候除了收集年輕代和老年代以外,也會將Humongous區一併回收。region
中的對象。由於G1
中年輕代和老年代都是以region
進行存儲的,因此年輕代和老年代均可以使用複製算法! 這種不會像CMS那樣回收完由於有不少內存碎片還須要整理一次,G1採用複製算法回收幾乎不會有太多內存碎片
G1的垃圾回收過程
G1由於在物理上已經不區分年輕代、老年代,因此邏輯上的年輕代,老年代都用的同一個垃圾收集器G1。
G1
使用原始快照解決漏標問題,而CMS
使用增量更新解決漏標問題
G1的垃圾收集分類
5%
,YoungGC
並非說Eden區滿了就馬上觸發,G1
會計算如今回收Eden須要多長時間,若是時間遠小於用戶設定的指望時間(使用-XX:MaxGCPauseMills
設定),就會給Eden
區擴容,直到擴容後的Eden
區再次放滿,再次計算。。。直到回收須要時長約等於用戶設定的指望停頓時間,此時纔會觸發YoungGC
!MixedGC
並非FullGC
,MixedGC的發生條件:經過-XX:InitiatingHeapOccupancyPercent
設置老年代的佔用比,默認是45%
,若是達到這個比例就觸發MixedGC,會回收Young、部分Old、Humongous區的對象。好比:堆默認有2048
個region,若是有接近1000
個region都是老年代的region,則可能就要觸發MixedGC
了,MixedGc
使用複製算法。須要把各個region中存活的對象拷貝到別的region裏去,拷貝過程當中若是發現沒有足夠的空region可以承載拷貝對象就會觸發一次真正的Full GC
!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
收集器的底層算法是比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是一款JDK 11
中新加入的具備實驗性質的低延遲垃圾收集器,在目前的jdk8
中並不適用!
ZGC的特色
10
ms,且不隨堆內存增大而增大!由於ZGC
中全部的垃圾收集階段幾乎都是併發執行!15%
,這個就很厲害了,G一、CMS都是經過延長回收時間來增長用戶體驗的!
ZGC的運做過程
併發重分配過程
問題:ZGC和G1在清理垃圾階段的區別是什麼?
zgc和g1的最大區別是在篩選回收階段,G1是GC線程併發執行清理,此時STW,修改對象引用很方便。ZGC是GC執行清理時和用戶線程併發操做,沒有stw,複雜度很高
顏色指針
以下圖所示,ZGC的核心設計之一。之前的垃圾回收器的GC信息都保存在對象頭中, 而ZGC的GC信息保存在指針中。
顏色指針的三大優點:
安全點
就是指代碼中一些特定的位置,當線程運行到這些位置時它的狀態是肯定的,這樣JVM就能夠安全的進行一些操做,好比GC等,因此GC不是想何時作就當即觸發的,是須要等待全部線程運行到安全點後才能觸發。若是馬上掛起全部用戶線程,可能會破壞某些用戶線程的原子性,好比:i++、jvm底層程序計數器的跳轉等。
大致實現思想是當垃圾收集須要中斷線程的時候, 不直接對線程操做, 僅僅簡單地設置一個標誌位, 各個線程執行過程 時會不停地主動去輪詢這個標誌, 一旦發現中斷標誌爲真時就本身在最近的安全點上主動中斷掛起。 輪詢標誌的地方和 安全點是重合的。
這些特定的安全點位置主要有如下幾種:
安全區域
若是一個線程處於 Sleep 或中斷狀態,它就不能掃描安全點,響應 JVM 的中斷請求。那麼他周圍的一片區域都是稱爲安全區域,這個區域的引用關係不會發生變化。在這個區域內的任意地方開始 GC 都是安全的。