當前位置:首頁 » 操作系統 » 垃圾回收演算法g1

垃圾回收演算法g1

發布時間: 2022-09-20 00:42:43

㈠ JVM G1參數

採用根搜索演算法,通過一系列名為」GC Roots」的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

1. 棧中引⽤的對象

2. 靜態變數、常量引⽤的對象

3. 本地⽅法棧native⽅法引⽤的對象

1.標記-復制

2.標記-清理

3.標記-整理

G1採取了不同的策略來解決並行、串列和CMS收集器的碎片、暫停時間不可控制等問題

G1會優先回收垃圾對象特別多的分區,這樣可以花費較少的時間來回收這些分區的垃圾

在年輕代回收期間,G1 GC 會調整其年輕代空間(eden 和存活空間大小)以滿足目標。

在混合回收期間,G1 GC 會根據混合垃圾回收的目標次數調整所回收的年老代區域數量,並調整堆的每個區域中存活對象的百分比,以及總體可接受的堆廢物百分比。

G1演算法將堆劃分為若干個區域(Region), 每個region可以是edon, survior, old區域,每個region大小相同為1M, 2M, 4M 2的冪次方大小,整個堆中默認有2048個region,每個Region默認按照512Kb劃分成多個Card。

如果一個對象佔用的空間大於一個region尺寸的一半,就會專門放入到humongous區域。G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。這種情況可以調整整個堆的大小,或者調整G1HeapRegionSize大小。

Remembered Set ,對應於一個region,採用point-in策略,記錄該region中某card被其它region 的引用情況。RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裡面的元素是Card Table的Index。 進行垃圾回收時,如果Region1有根對象A引用了Region2的對象B,顯然對象B是活的,如果沒有Rset,就需要掃描整個Region1或者其它Region,才能確定對象B是活躍的,有了Rset可以避免對整個堆進行掃描。

cms中年老代也有rset,採用point-out策略,記錄年老代中引用年輕代的對象,這樣在ygc時就不用掃描整個年老代,只掃描年老代的rset。

G1MaxNewSizePercent 新生代最大值,默認值60%

G1MaxPauseTime 設置G1收集過程目標時間,默認值200ms

G1ReservePercent  預留百分之多少內存,防止晉升失敗的情況,默認值是10

-XX:=45 – 整個堆棧使用達到百分之多少的時候,啟動GC周期. 基於整個堆,不僅僅是其中的某個代的佔用情況,G1根據這個值來判斷是否要觸發GC周期, 0表示一直都在GC,默認值是45(即45%滿了,或者說佔用了),啟動mix gc

MaxRAMPercentage、InitialRAMPercentage、MinRAMPercentage 應用於docker容器中,根據docker容器內存大小指定堆的初始,最大,最小比例

ParallelRefProcEnabled 默認為false,並行的處理Reference對象,如WeakReference,除非在GC log里出現Reference處理時間較長的日誌,否則效果不會很明顯

顯式的使用-Xmn設置年輕代的大小,會干預G1的默認行為。

G1就不會再考慮設定的暫停時間目標,所以本質上說,設定了年輕代大小就相當於禁用了目標暫停時間。

G1就無法根據需要增大或者縮小年輕代的小心。既然大小固定了,就無法在大小上做任何改變了。

為了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即mixed gc

除了回收整個young region,還會回收一部分的old region

主要分為以下幾個步驟:

1. initial mark: 初始標記過程,整個過程STW,標記了從GC Root可達的對象

2. concurrent marking: 並發標記過程,整個過程gc collector線程與應用線程可以並行執行,標記出GC Root可達對象衍生出去的存活對象,並收集各個Region的存活對象信息

3. remark: 最終標記過程,整個過程STW,標記出那些在並發標記過程中遺漏的,或者內部引用發生變化的對象

4. clean up: 垃圾清除過程,如果發現一個Region中沒有存活對象,則把該Region加入到空閑列表中

使用范圍不一樣

CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用

G1收集器收集范圍是老年代和新生代

STW的時間

CMS收集器以最小的停頓時間為目標的收集器。

G1收集器可預測垃圾回收的停頓時間(建立可預測的停頓時間模型)

垃圾碎片

CMS收集器是使用「標記-清除」演算法進行的垃圾回收,容易產生內存碎片

G1收集器使用的是「標記-整理」演算法,進行了空間整合,降低了內存空間碎片

參考: https://www.jianshu.com/p/a3e6a9de7a5d

https://blog.csdn.net/u013380694/article/details/83341913

https://www.jianshu.com/p/ab54489f5d71?u_atoken=ca2d26ce-15a4-462b-9ee2-1d3dfca2d647&u_asession=-kRD0-eOdne8XcfWhUbbJUSLGdkUER_tKV6ZX0KNBwm7Lovlpxjd_P_q4JsKWYrT3W_NKPr8w6oU7K9iRp8G_&u_asig=_7vZwmCUEmowKET9soS-B3_YnquxJK1II_ufphdjR9EF5W4qBzbaQxa_DPpZ9KH_-QE6N5IgXkZa79JS7q8ZD7Xtz2Ly--WWPRPQyB_SKrj-61LB_f61u3h9VXwMyh6PgyDIVSG1W__la6lRJ-&u_aref=npKvxxWi1kDXXuw5mG2TYBN3CXA%3D

㈡ CMS 和G1 的區別

CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
G1收集器收集范圍是老年代和新生代。不需要結合其他收集器使用

CMS收集器以最小的停頓時間為目標的收集器。

G1收集器可預測垃圾回收的停頓時間(建立可預測的停頓時間模型)

CMS收集器是使用「標記-清除」演算法進行的垃圾回收,容易產生內存碎片

G1收集器使用的是「標記-整理」演算法,進行了空間整合,降低了內存空間碎片。

初始標記
並發標記
重新標記
並發清理

初始標記階段:會讓線程全部停止,也就是 Stop the World 狀態
並發標記階段:對所有的對象進行追蹤,這個階段最耗費時。但這個階段是和系統並發運行的,所以不會對系統運行造成影響
重新標記階段:由於第二階段是並發執行的,一邊標記垃圾對象,一邊創建新對象,老對象會變成垃圾對象。 所以第三階段也會進入 Stop the World 狀態,並且重新標記,標記的是第二階段中變動過的少數對象,所以運行速度很快
並發清理階段: 這個階段也是會耗費很多時間,但由於是並發運行的,所以對系統不會造成很大的影響

CMS採用 標記-清理 的演算法,標記出垃圾對象,清除垃圾對象。演算法是基於老年代執行的,因為新生代產生無法接受該演算法產生的碎片垃圾。

優點 :並發收集,低停頓

不足

G1的出現就是為了替換jdk1.5種出現的CMS,這一點已經在jdk9的時候實現了,jdk9默認使用了G1回收器,移除了所有CMS相關的內容。G1和CMS相比,有幾個特點:

G1把java內存拆分成多等份,多個域(Region),邏輯上存在新生代和老年代的概念,但是沒有嚴格區分

貼圖感受一下:

依舊存在新生代老年代的概念,但是沒有嚴格區分。Region最多分為2048個

除了上面優點之外,還有一個優點,那就是對大對象的處理。在CMS內存中,如果一個對象過大,進入S1、S2區域的時候大於改分配的區域,對象會直接進入老年代。G1處理大對象時會判斷對象是否大於一個Region大小的50%,如果大於50%就會橫跨多個Region進行存放

初始標記: 標記GC Roots 可以直接關聯的對象,該階段需要線程停頓但是耗時短

並發標記: 尋找存活的對象,可以與其他程序並發執行,耗時較長

最終標記: 並發標記期間用戶程序會導致標記記錄產生變動(好比一個阿姨一邊清理垃圾,另一個人一邊扔垃圾)虛擬機會將這段時間的變化記錄在Remembered Set Logs 中。最終標記階段會向Remembered Set合並並發標記階段的變化。這個階段需要線程停頓,也可以並發執行

篩選回收: 對每個Region的回收成本進行排序,按照用戶自定義的回收時間來制定回收計劃

參考官方文檔:

控制G1回收垃圾的時間

-XX:MaxGCPauseMillis=200 (默認200ms)

㈢ JVM的垃圾演算法有哪幾種

一、垃圾收集器概述

如上圖所示,垃圾回收演算法一共有7個,3個屬於年輕代、三個屬於年老代,G1屬於橫跨年輕代和年老代的演算法。

JVM會從年輕代和年老代各選出一個演算法進行組合,連線表示哪些演算法可以組合使用

二、各個垃圾收集器說明

1、Serial(年輕代)

  • 年輕代收集器,可以和Serial Old、CMS組合使用

  • 採用復制演算法

  • 使用單線程進行垃圾回收,回收時會導致Stop The World,用戶進程停止

  • client模式年輕代默認演算法

  • GC日誌關鍵字:DefNew(Default New Generation)

  • 圖示(Serial+Serial Old)

    7、G1

  • G1收集器由於沒有使用過,所以從網上找了一些教程供大家了解

  • 並行與並發

  • 分代收集

  • 空間整合

  • 可預測的停頓

㈣ JVM之G1垃圾回收器


stop the world ,這個是最痛的一個點!無論是新生代垃圾回收,還是老年代垃圾回收,都會或多或少產生「stop the world」現象,對系統的運行時有一定影響的。所以其實之後對垃圾回收器的優化,都是朝著減少「stop the world」的目標去做的。在這個基礎上,G1垃圾回收器就應運而生,它可以提供比 組合更好的垃圾回收的性能。

G1垃圾回收器是可以同時回收新生代和老年代的對象的,不需要兩個垃圾回收器配合起來運作,它一個人就可以搞定所有的垃圾回收。它最大的一個特點,就是把java堆內存拆分為多個大小相等的Region ,如下圖:


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

簡單來說,G1可以做到讓你來設定垃圾回收對系統的影響,它自己通過把內存拆分為大量小Region,以及追蹤每個Region中可以回收的對象大小和預估時間,最後在垃圾回收的時候,盡量把垃圾回收對系統造成的影響控制在你指定的時間范圍內,同時在有限的時間內盡量回收盡可能多的垃圾對象,這就是 G1的核心設計思路


剛開始Region可能誰都不屬於,然後就分配給了新生代,放了很多屬於新生代的對象,接著觸發了垃圾回收這個Region,然後下一次這個Region可能又被分配給了老年代,用來放老年代長期存活的對象。所以G1對應的內存模型中,Region隨時會屬於新生代,也會屬於老年代,所以沒有所謂新生代給多少內存,老年代給多少內存這一說了。實際上新生代和老年代各自的內存區域是不停的變動的,由G1自動控制。


我們現在來思考兩個問題:
1、到底有多少個Region?
2、每個Region的大小是多大呢?
其實默認情況下是自動計算和設置的,我們可以給整個堆內存設置一個大小,比如說用「-Xms」和「-Xmx」來設置堆內存的大小。然後jvm啟動的時候一旦發現你使用的是G1垃圾回收器,可以使用「-XX:UserG1GC」來指定使用G1垃圾回收器,此時會自動用堆大小除以2048,因為jvm最多可以有2048個Region,然後Region的大小必須是2的倍數,比如說2MB、4MB之類的。大概就是這樣子來決定Region的數量和大小的,大家一般保持默認的計算方式就可以。如果通過手動方式來指定,則可以通過「-XX:G1HeapRegionSize」參數來設置。

剛開始的時候,默認新生代堆內存的佔比是5%,也就是占據200MB左右的內存,對應大概是100個Region,這個可以通過「-XX:G1NewSizePercent」來設置新生代的初始佔比,其實維持這個默認值即可。因為在系統運行中,jvm其實會不停的給新生代增加更多的Region,但是新生代的佔比最多不會超過60%,但是可以通過「-XX:G1MaxNewSizePercent」參數來調整比例。而且一旦Region進行了垃圾回收,此時新生代的Region數量還會減少,這些其實都是動態的。


其實在G1中雖然把內存劃分為了很多的Region,但還是有新生代、老年代的區分。而且新生代里還是有Eden和Survivor的劃分的,所以大家會發現之前學習的很多技術原理在G1時期都是有用的。大家應該還記得之前說過的一個新生代的參數,「-XX:SurvivorRatio=8」,所以這里還是可以區分出來屬於新生代的Region里哪些屬於Eden區,哪些屬於Survivor區。比如之前說新生代剛開始的時候,有100個Region,那麼可能80個Region就是Eden區,兩個Survivor區各自佔10個Region。因為新生代的Region數量是動態的,所以隨著對象不停的在新生代里分配,屬於新生代的Region會不斷增加,Eden和Survivor對應的Region也會不斷增加。

既然G1的新生代也有Eden和Survivor的區分,那麼觸發垃圾回收的機制都是類似的。隨著不停的在新生代的Eden區對應的Region中放對象,jvm就會不停的給新生代加入更多的Region,直到新生代占據堆大小的最大比例60%為止。一旦新生代達到了設定的占據堆內存的最大大小60%,這個時候就會觸發新生代的GC,G1就會用之前說過的復制演算法來進行垃圾回收,進入一個「stop the world」狀態,然後把Eden區對應的Region中的存活對象放入S1區對應的Region中,接著回收掉Eden區對應的Region中的垃圾對象。 ,因為G1是可以設定目標GC停頓時間的,也就是G1執行GC的時候最多可以讓系統停頓多長時間,可以通過「-XX:MaxGCPauseMills」參數來設定,默認值是200ms,那麼在程序運行期間G1會根據你設定的gc停頓時間給新生代不停分配Region,然後到一定程度,就會觸發新生代gc,保證新生代gc的時候導致的系統停頓時間在你預設的范圍內,當然這個數字並不是那麼的精準。


我們都知道,在G1的內存模型下,新生代和老年代各自都會占據一定的Region,老年代也會有自己的Region。按照默認新生代最大隻能占據堆內存60%的Region來推算,老年代最多可以占據40%的Region,大概就是800個左右的Region。
可以說跟之前幾乎是一樣的,還是這么幾個條件:
1、對象在新生代躲過了很多次的垃圾回收,達到了一定的年齡了它就會進入老年代,「-XX:MaxTenuringThreshold」參數可以設置這個年齡。
2、動態年齡判定規則,如果一旦發現某次新生代GC過後,存活對象超過了Survivor的50%,此時就會判斷一下,比如年齡為1歲,2歲,3歲,4歲的對象的大小總和超過了Survivor的50%,此時4歲以上的對象全部會進入老年代,這就是動態年齡判定規則。


以前說是那種大對象也是可以直接進入老年代的,那麼現在在G1這套內存模型下呢?實際上這里會有所改變,G1提供了專門的Region來存放大對象,而不是讓大對象進入老年代中的Region。在G1中,大對象的判斷規則就是一個大對象超過了一個Region大小的50%,而且一個大對象如果太大,可能會橫跨多個Region來存放。 不是說60%給新生代,40%給老年代嗎,那還有Region給大對象?我們現在知道,在G1里,新生代和老年代的Region是不停變化的。比如新生代現在占據了1200個Region,但是一次垃圾回收之後,就讓裡面1000個Region都空了,此時那1000個Region就可以不屬於新生代了,裡面很多Region可以用來存放大對象。那麼大對象既然不屬於新生代和老年代,那麼什麼時候會觸發垃圾回收呢?其實新生、老年代在回收的時候,會順帶帶著大對象Region一起回收,所以,這就是在G1內存模型下對大對象的分配和回收的策略。


G1有一個參數是「-XX:」,它的默認值是45%,意思是說,如果老年代占據了堆內存的45%的Region的時候,此時會嘗試觸發一次新生代+老年代一起回收的混合回收階段。

,這個過程是需要進入「stop the world」的,但這個過程僅僅只是標記一下GC Roots直接能引用的對象,所以速度是很快的。 ,這個階段會允許系統程序運行,同時進行GC Roots追蹤,從GC Roots開始追蹤所有的存活對象,這個過程加入了對間接引用對象的追蹤,這個過程前面的文章已經介紹過了,這里就不再贅述。 ,這個階段會進入「stop the world」,系統程序會禁止運行,最終標記一下有哪些是存活對象,有哪些是垃圾對象。 ,這個階段會計算老年代中每個Region中的存活對象數量,存活對象佔比,還有執行垃圾回收的預期性能和效率。接著會停止系統程序,然後全力以赴進行垃圾回收,此時會選擇部分Region進行回收,因為必須讓垃圾回收的停頓時間控制在我們指定的范圍內,所以說它會從新生代、老年代、大對象里各自挑選一些Region,保證用指定的時間回收盡可能多的垃圾,這就是所謂的混合回收。

G1垃圾回收器的一些參數
最後一個階段混合回收的時候,會停止所有程序運行,所以說G1是允許執行多次混合回收。比如先停止工作,執行一次混合回收,回收掉一些Region,接著恢復系統運行,然後再次停止系統運行,再執行一次混合回收,回收掉一些Region。有一些參數可以控制這個,比如「-XX:G1MixedGCCountTarget」參數,就是一次混合回收的過程中,最後一個階段執行幾次混合回收,默認值是8次。這樣的好處是不讓系統停止的時間過長。還有一個參數就是「-XX:G1HeapWastePercent」,默認值是5%,它的意思是說,在回收的過程中會不斷空出來新的Region,一旦空閑出來的Region數量達到了堆內存的5%,此時就會立即停止混合回收,意味著本次混合回收就結束了。
G1整體是基於復制演算法進行Region垃圾回收的,不會出現內存碎片的問題,不需要像CMS那樣標記-清理之後再進行內存碎片的整理。
還有一個參數,「-XX:G1MixedGCLiveThresholdPercent」,它的默認值是85%,意思是確定要回收的Region的時候,必須是存活對象低於85%的Region才可以進行回收,否則要是一個Region的存活對象多餘85%,回收它也作用不大,而且還要把85%的對象都拷貝到別的Region,這個成本是很高的。


萬一出現拷貝的過程中發現沒有空閑Region可以承載自己的存活對象了,就會觸發一次失敗。一旦失敗,立馬就會切換為停止系統程序,然後採用單線程進行標記、清理和壓縮整理,空閑出來一批Region,這個過程是極慢的。

G1垃圾回收器的使用場景
當你的系統部署在大內存機器上的時候,比如說你的機器是32核64G的機器,此時你分配給系統的內存有幾十個G,新生代的Eden區可能30~40G的內存。比如類似kafka、elasticsearch之類的大數據相關系統,都是部署在大內存的機器上的。此時如果你的系統負載非常的高,比如每秒幾萬的訪問請求到kafka、elasticsearch上去,那麼可能導致你Eden區的幾十G內存頻繁塞滿,然後要觸發垃圾回收,假設1分鍾會塞滿一次。然後每次垃圾回收要停掉kafka、elasticsearch的運行,然後執行垃圾回收大概需要幾秒鍾,此時你發現,可能每過一分鍾,你的系統就要卡頓幾秒鍾,有的請求一旦卡死幾秒鍾就會超時報錯,此時可能會導致你的系統頻繁出錯。 !針對G1垃圾回收器,我們可以設置每次GC的停頓時間,比如我們設置100ms,那麼每次垃圾回收我們的系統最多也就停頓100ms,然後系統繼續運行。G1天生就適合這種大內存機器的jvm運行。

本文結束。

㈤ 面試官:談談你對G1垃圾收集器有哪些了解

作為一款高效的垃圾收集器,G1在JDK7中加入JVM,在JDK9中取代CMS成為了默認的垃圾收集器。

新生代採用復制演算法,主要的垃圾收集器有三個,Serial、Parallel New 和 Parallel Scavenge,特性如下:

G1垃圾收集器主要用於多處理器、大內存的場景,它有五個屬性:分代、增量、並行(大多時候可以並發)、stop the word、標記整理。

我們知道,垃圾收集器的一個目標就是STW(stop the word)越短越好。利用可預測停頓時間模型,G1為垃圾收集設定一個STW的目標時間(通過 -XX:MaxGCPauseMillis 參數設定,默認200ms),G1盡可能地在這個時間內完成垃圾收集,並且在不需要額外配置的情況下實現高吞吐量。

G1致力於在下面的應用和環境下尋找延遲和吞吐量的最佳平衡:

如果在JDK8中使用G1,我們可以使用參數 -XX:+UseG1GC 來開啟。

G1把整個堆分成了大小相等的region,每一個region都是連續的虛擬內存,region是內存分配和回收的基本單位。如下圖:

紅色帶"S"的region表示新生代的survivor,紅色不帶"S"的表示新生代eden,淺藍色不帶"H"的表示老年代,淺藍色帶"H"的表示老年代中的大對象。跟G1之前的內存分配策略不同的是,survivor、eden、老年代這些區域可能是不連續的。

G1在停頓的時候可以回收整個新生代的region,新生代region的對象要不復制到survivor區要不復制到老年代region。同時每次停頓都可以回收一部分老年代的內存,把老年代從一個region復制到另一個region。

上一節我們看到,整個堆內存被G1分成了多個大小相等的region,每個堆大約可以有2048個region,每個region大小為 1~32 MB(必須是2的次方)。region的大小通過 -XX:G1HeapRegionSize 來設置,所以按照默認值來G1能管理的最大內存大約 32MB * 2048 = 64G。

大對象是指大小超過了region一半的對象,大對象可以橫跨多個region,給大對象分配內存的時候會直接分配在老年代,並不會分配在eden區。

如下圖,一個大對象占據了兩個半region,給大對象分配內存時,必須從一個region開始分配連續的region,在大對象被回收前,最後一個region不能被分配給其他對象。

大對象什麼時候回收 ?通常,只有在mark結束以後的Cleanup停頓階段或者FullGC的時候,死亡的大對象才會被回收掉。但是,基本類型(比如bool數組、所有的整形數組、浮點型數組等)的數組大對象有個例外,G1會在任何GC停頓的時候回收這些死亡大對象。這個默認是開啟的,但是可以使用
-XX: 這個參數禁用掉。

分配大對象的時候,因為佔用空間太大,可能會過早發生GC停頓。G1在每次分配大對象的時候都會去檢查當前堆內存佔用是否超過初始堆佔用閾值IHOP(The Initiating Heap Occupancy Percent),如果當前的堆佔用率超過了IHOP閾值,就會立刻觸發 initial mark。 關於initial mark詳見第4節

即使是在FullGC的時候,大對象也是永遠不會被移動的。這可能導致過早發生FullGC或者是意外的OOM,因為此時雖然還有大量的空閑內存,但是這些內存都是region中的內存碎片。

G1雖然把堆內存劃分成了多個region,但是依然存在新生代和老年代的概念。G1新增了2個控制新生代內存大小的參數,-XX:G1NewSizePercent(默認等於5),-XX:G1MaxNewSizePercent(默認等於60)。也就是說新生代大小默認占整個堆內存的 5% ~ 60%。

根據前面介紹,一個堆大概可以分配2048個region,每個region最大32M,這樣G1管理的整個堆的大小最大可以是64G,新生代佔用的大小范圍是 3.2G ~ 38.4G。

對於 -XX:G1NewSizePercent 和 -XX:G1MaxNewSizePercent,下面幾個問題需要注意:

生效,比如堆大小是64G,設置 -Xmn3.2G,那麼就等價於 -XX:G1NewSizePercent=5 並且 -XX:G1MaxNewSizePercent=5,因為3.2G/64G = 5%。

生效,比如堆大小是64G,設置 -XX:NewRatio=3,那麼就等價於 -XX:G1NewSizePercent=25 並且 -XX:G1MaxNewSizePercent=25。因為年輕代:老年代 = 1 :3,說明年輕代佔1/4 = 25%。

設置的這個參數不生效,兩個參數都用默認值。

跟 -XX:GCTimeRatio 這個參數相關。這個參數為0~100之間的整數(G1默認是9, 其它收集器默認是99),值為 n 則系統將花費不超過 1/(1+n) 的時間用於垃圾收集。因此G1默認最多 10% 的時間用於垃圾收集,如果垃圾收集時間超過10%,則觸發擴容。如果擴容失敗,則發起Full GC。

G1的垃圾收集是在 Young-Only 和 Space-Reclamation兩個階段交替執行的。如下圖:

young-only階段會用對象逐步把老年代區域填滿,space-reclamation階段除了會回收年輕代的內存以外,還會增量回收老年代的內存。完成後重新開始young-only階段。

Young-only階段流程如下圖:

這個階段從普通的 young-only GC 開始,young-only GC把一些對象移動到老年代,當老年代的空間佔用達到IHOP時,G1就停止普通的young-only GC,開始初始標記(Initial Mark)。

在young-only階段,要回收新生代的region。每一次 young-only 結束的時候,G1總是會調整新生代大小。G1可以使用參數 -XX:MaxGCPauseTimeMillis和
-XX:PauseTimeIntervalMillis 來設置目標停頓時間,這兩個參數是對實際停頓時間的長期觀察得來的。他會根據在GC的時候要拷貝多少個對象,對象之間是如何相互關聯的等信息計算出來回收相同大小的新生代內存需要花費多少時間,

如果沒有其他的限定條件,G1會把young區的大小調整為 -XX:G1NewSizePercent和 -XX:G1MaxNewSizePercent 之間的值來滿足停頓時間的要求。

這個階段由多個Mixed GC組成,不光回收年輕代垃圾,也回收老年代垃圾。當 G1 發現回收更多的老年代區域不能釋放更多空閑空間時,這個階段結束。之後,周期性地再次開啟一個新的Young-only階段。

當G1收集存活對象信息時內存不足,G1會做一個Full GC,並且會STW。

在 space-reclamation 階段,G1會盡量在GC停頓時間內回收盡可能多的老年代內存。這個階段新生代內存大小被調整為 -XX:G1NewSizePercent 設置的允許的最小值,只要存在可回收的老年代region就會被添加到回收集合中,直到再添加會超出目標停頓時間為止。在特定的某個GC停頓時間內,G1會按照這老年代region回收的效率(效率高的優先收集)和剩餘可用時間來得到最終待回收region集合。

每一個GC停頓期間要回收的老年代region數量受限於候選region集合數量除以 -XX:G1MixedGCCountTarget 這個參數值,參數 -XX:G1MixedGCCountTarget 指定一個周期內觸發Mixed GC最大次數,默認值8。比如 -XX:G1MixedGCCountTarget 採用默認值8,候選region集合有200個region,那每次停頓期間收集25個region。

當待回收region集合中可回收的空間佔用率低於參數值 -XX:G1HeapWastePercent 的時候,Space-Reclamation結束。

當應用存活對象佔用了大量內存,以至於回收剩餘對象沒有足夠的空間拷貝時,就會觸發 evacuation failure。這時G1為了完成當前的垃圾收集,會保留已經位於新的位置上的存活對象不動,對於沒有移動和拷貝的對象就不會進行拷貝了,僅僅調整對象間的引用。

evacuation failure會導致一些額外的開銷,但是一般會跟其他 young GC 一樣快。evacuation failure完成以後,G1會跟正常情況下一樣繼續恢復應用的執行。G1會假設 evacuation failure是發生在GC的後期,這時大部分對象已經移動過了,並且已經有足夠的內存來繼續執行應用程序一直到 mark 結束 space-reclamation 開始。如果這個假設不成立(也就是說沒有足夠的內存來執行應用程序),G1最終只能發起Full GC,對整個堆做壓縮,這個過程可能會非常慢。

Parallel GC 可以壓縮和回收老年代的內存,但是也只能對老年代整體來操作。G1以增量的方式把整個GC工作增量的分散到多個更短的停頓時間中,當然這可能會犧牲一定吞吐量。

跟CMS類似,G1並發回收老年代內存,但是,CMS採用標記-清除演算法,不會處理老年代的內存碎片,最終就會導致長時間的FullGC。

因為採用並發收集,G1的性能開銷會更大,這可能會影響吞吐量。

G1在任何的GC期間都可以回收老年代中全空或者佔用大空間的內存。這可以避免一些不必要的GC,因為可以非常輕易地釋放大量的內存空間。這個功能默認開啟,可以採用
-XX:- 參數關閉。

G1可以選擇對整個堆裡面的String進行並行去重。這個功能默認關閉,可以使用參數 -XX:+
G1EnableStringDeplication 來開啟。

本文詳細介紹了G1垃圾收集器,希望能夠對你理解G1有所幫助。

㈥ G1從入門到放棄(一)

最近在看關於G1垃圾收集的文章,看了很多國內與國外的資料,本文對G1的這些資料進行了整理。這篇合適JVM垃圾回收有一定基礎的同學,作為G1入門可以看一下,如果要死磕G1實現的內容細節。大家可以找 R大 。 個人認為R大是目前國內JVM領域研究的先驅了,當然R大也是不建議大家去看JVM的源碼的。 為啥別讀HotSpot VM的源碼
G1系列第一篇文章會介紹G1的理論知識,不會做JVM源碼的深入分析。第二篇准備介紹G1實踐中的日誌分析。

G1(Garbadge First Collector)作為一款JVM最新的垃圾收集器,可以解決CMS中Concurrent Mode Failed問題,盡量縮短處理超大堆的停頓,在G1進行垃圾回收的時候完成內存壓縮,降低內存碎片的生成。G1在堆內存比較大的時候表現出比較高吞吐量和短暫的停頓時間,而且已成為Java 9的默認收集器。未來替代CMS只是時間的問題。

G1的內存結構和傳統的內存空間劃分有比較的不同。G1將內存劃分成了多個大小相等的Region(默認是512K),Region邏輯上連續,物理內存地址不連續。同時每個Region被標記成E、S、O、H,分別表示Eden、Survivor、Old、Humongous。其中E、S屬於年輕代,O與H屬於老年代。
示意圖如下:

H表示Humongous。從字面上就可以理解表示大的對象(下面簡稱H對象)。 當分配的對象大於等於Region大小的一半 的時候就會被認為是巨型對象。H對象默認分配在老年代,可以防止GC的時候大對象的內存拷貝。通過如果發現堆內存容不下H對象的時候,會觸發一次GC操作。

在進行Young GC的時候,Young區的對象可能還存在Old區的引用, 這就是跨代引用的問題。為了解決Young GC的時候,掃描整個老年代,G1引入了 Card Table 和 Remember Set 的概念,基本思想就是用空間換時間。這兩個數據結構是專門用來處理Old區到Young區的引用。Young區到Old區的引用則不需要單獨處理,因為Young區中的對象本身變化比較大,沒必要浪費空間去記錄下來。

下圖展示的是 RSet 與 Card 的關系。每個 Region 被分成了多個 Card ,其中綠色部分的 Card 表示該 Card 中有對象引用了其他 Card 中的對象,這種引用關系用藍色實線表示。 RSet 其實是一個HashTable,Key是Region的起始地址,Value是 Card Table (位元組數組),位元組數組下標表示 Card 的空間地址,當該地址空間被引用的時候會被標記為 dirty_card 。

關於RSet結構的維護,可以參考這篇 文章 ,這里不做過多的深入。

SATB的全稱(Snapshot At The Beginning)字面意思是開始GC前存活對象的一個快照。SATB的作用是保證在並發標記階段的正確性。如何理解這句話?
首先要介紹三色標記演算法。

在GC掃描C之前的顏色如下:

在並發標記階段,應用線程改變了這種引用關系

得到如下結果。

在重新標記階段掃描結果如下

這種情況下C會被當做垃圾進行回收。Snapshot的存活對象原來是A、B、C,現在變成A、B了,Snapshot的完整遭到破壞了,顯然這個做法是不合理。
G1採用的是 pre-write barrier 解決這個問題。簡單說就是在並發標記階段,當引用關系發生變化的時候,通過 pre-write barrier 函數會把這種這種變化記錄並保存在一個隊列里,在JVM源碼中這個隊列叫 satb_mark_queue 。在remark階段會掃描這個隊列,通過這種方式,舊的引用所指向的對象就會被標記上,其子孫也會被遞歸標記上,這樣就不會漏標記任何對象,snapshot的完整性也就得到了保證。

這里引用R大對SATB的解釋:

SATB的方式記錄活對象,也就是那一時刻對象snapshot, 但是在之後這裡面的對象可能會變成垃圾, 叫做浮動垃圾(floating garbage),這種對象只能等到下一次收集回收掉。在GC過程中新分配的對象都當做是活的,其他不可達的對象就是死的。
如何知道哪些對象是GC開始之後新分配的呢?
在Region中通過top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS來記錄新配的對象。示意圖如下:

每個region記錄著兩個top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。 這里引用R大的解釋。

其中top是該region的當前分配指針,[bottom, top)是當前該region已用(used)的部分,[top, end)是尚未使用的可分配空間(unused)。
(1): [bottom, prevTAMS): 這部分里的對象存活信息可以通過prevBitmap來得知
(2): [prevTAMS, nextTAMS): 這部分里的對象在第n-1輪concurrent marking是隱式存活的
(3): [nextTAMS, top): 這部分里的對象在第n輪concurrent marking是隱式存活的

Young GC 回收的是所有年輕代的Region。 當E區不能再分配新的對象時就會觸發 。E區的對象會移動到S區,當S區空間不夠的時候,E區的對象會直接晉升到O區,同時S區的數據移動到新的S區,如果S區的部分對象到達一定年齡,會晉升到O區。
Yung GC過程示意圖如下:

Mixed GC 翻譯過來叫混合回收。之所以叫混合是因為回收所有的年輕代的Region+部分老年代的Region。
1、為什麼是老年代的 部分 Region?
2、什麼時候觸發Mixed GC?
這兩個問題其實可以一並回答。回收 部分 老年代是參數 -XX:MaxGCPauseMillis ,用來指定一個G1收集過程目標停頓時間,默認值200ms,當然這只是一個期望值。G1的強大之處在於他有一個停頓預測模型(Pause Prediction Model),他會有選擇的挑選 部分 Region,去盡量滿足停頓時間,關於G1的這個模型是如何建立的,這里不做深究。
Mixed GC的觸發也是由一些參數控制。比如 XX: 表示老年代占整個堆大小的百分比,默認值是45%,達到該閾值就會觸發一次Mixed GC。

Mixed GC主要可以分為兩個階段:
1、全局並發標記(global concurrent marking)
全局並發標記又可以進一步細分成下面幾個步驟:

2、拷貝存活對象(Evacuation)
Evacuation階段是全暫停的。它負責把一部分region里的活對象拷貝到空region里去(並行拷貝),然後回收原本的region的空間。Evacuation階段可以自由選擇任意多個region來獨立收集構成收集集合(collection set,簡稱CSet),CSet集合中Region的選定依賴於上文中提到的 停頓預測模型 ,該階段並不evacuate所有有活對象的region,只選擇收益高的少量region來evacuate,這種暫停的開銷就可以(在一定范圍內)可控。

Mixed GC的清理過程示意圖如下:

G1的垃圾回收過程是和應用程序並發執行的,當Mixed GC的速度趕不上應用程序申請內存的速度的時候,Mixed G1就會降級到Full GC,使用的是Serial GC。Full GC會導致長時間的STW,應該要盡量避免。
導致G1 Full GC的原因可能有兩個:

PS: 本文主要參考的國內文章:
java Hotspot G1 GC的一些關鍵技術
Garbage First G1收集器 理解和原理分析
G1: One Garbage Collector To Rule Them All
請教G1演算法的原理
深入理解 Java G1 垃圾收集器
Getting Started with the G1 Garbage Collector !

㈦ jvm的理解

1
JVM內存區域

我們在編寫程序時,經常會遇到OOM(out of Memory)以及內存泄漏等問題。為了避免出現這些問題,我們首先必須對JVM的內存劃分有個具體的認識。JVM將內存主要劃分為:方法區、虛擬機棧、本地方法棧、堆、程序計數器。JVM運行時數據區如下:

1.1
程序計數器
程序計數器是線程私有的區域,很好理解嘛~,每個線程當然得有個計數器記錄當前執行到那個指令。佔用的內存空間小,可以把它看成是當前線程所執行的位元組碼的行號指示器。如果線程在執行Java方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令地址;如果執行的是Native方法,這個計數器的值為空(Undefined)。

此內存區域是唯一一個在Java虛擬機規范中沒有規定任何OutOfMemoryError情況的區域。

1.2
Java虛擬機棧
與程序計數器一樣,Java虛擬機棧也是線程私有的,其生命周期與線程相同。

如何理解虛擬機棧呢?

本質上來講,就是個棧。裡面存放的元素叫棧幀,棧幀好像很復雜的樣子,其實它很簡單!它裡面存放的是一個函數的上下文,具體存放的是執行的函數的一些數據。執行的函數需要的數據無非就是局部變數表(保存函數內部的變數)、操作數棧(執行引擎計算時需要),方法出口等等。

執行引擎每調用一個函數時,就為這個函數創建一個棧幀,並加入虛擬機棧。換個角度理解,每個函數從調用到執行結束,其實是對應一個棧幀的入棧和出棧。

注意這個區域可能出現的兩種異常:

一種是StackOverflowError,當前線程請求的棧深度大於虛擬機所允許的深度時,會拋出這個異常。製造這種異常很簡單:將一個函數反復遞歸自己,最終會出現棧溢出錯誤(StackOverflowError)。

另一種異常是OutOfMemoryError異常,當虛擬機棧可以動態擴展時(當前大部分虛擬機都可以),如果無法申請足夠多的內存就會拋出OutOfMemoryError,如何製作虛擬機棧OOM呢,參考一下代碼:

這段代碼有風險,可能會導致操作系統假死,請謹慎使用~~~

1.3
本地方法棧
本地方法棧與虛擬機所發揮的作用很相似,他們的區別在於虛擬機棧為執行Java代碼方法服務,而本地方法棧是為Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

1.4
Java堆
Java堆可以說是虛擬機中最大一塊內存了。它是所有線程所共享的內存區域,幾乎所有的實例對象都是在這塊區域中存放。當然,隨著JIT編譯器的發展,所有對象在堆上分配漸漸變得不那麼「絕對」了。

Java堆是垃圾收集器管理的主要區域。由於現在的收集器基本上採用的都是分代收集演算法,所有Java堆可以細分為:新生代和老年代。在細致分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間。當堆無法再擴展時,會拋出OutOfMemoryError異常。

1.5
方法區
方法區存放的是類信息、常量、靜態變數等。方法區是各個線程共享區域,很容易理解,我們在寫Java代碼時,每個線程度可以訪問同一個類的靜態變數對象。由於使用反射機制的原因,虛擬機很難推測那個類信息不再使用,因此這塊區域的回收很難。另外,對這塊區域主要是針對常量池回收,值得注意的是JDK1.7已經把常量池轉移到堆裡面了。同樣,當方法區無法滿足內存分配需求時,會拋出OutOfMemoryError。

製造方法區內存溢出,注意,必須在JDK1.6及之前版本才會導致方法區溢出,原因後面解釋,執行之前,可以把虛擬機的參數-XXpermSize和-XX:MaxPermSize限制方法區大小。

運行後會拋出java.lang.OutOfMemoryError:PermGen space異常。

解釋一下,String的intern()函數作用是如果當前的字元串在常量池中不存在,則放入到常量池中。上面的代碼不斷將字元串添加到常量池,最終肯定會導致內存不足,拋出方法區的OOM。

下面解釋一下,為什麼必須將上面的代碼在JDK1.6之前運行。我們前面提到,JDK1.7後,把常量池放入到堆空間中,這導致intern()函數的功能不同,具體怎麼個不同法,且看看下面代碼:

這段代碼在JDK1.6和JDK1.7運行的結果不同。

JDK1.6結果是:false,false ,JDK1.7結果是true, false。

原因是:JDK1.6中,intern()方法會吧首次遇到的字元串實例復制到常量池中,返回的也是常量池中的字元串的引用,而StringBuilder創建的字元串實例是在堆上面,所以必然不是同一個引用,返回false。

在JDK1.7中,intern不再復制實例,常量池中只保存首次出現的實例的引用,因此intern()返回的引用和由StringBuilder創建的字元串實例是同一個。為什麼對str2比較返回的是false呢?這是因為,JVM中內部在載入類的時候,就已經有"java"這個字元串,不符合「首次出現」的原則,因此返回false。

熱點內容
內置存儲卡可以拆嗎 發布:2025-05-18 04:16:35 瀏覽:336
編譯原理課時設置 發布:2025-05-18 04:13:28 瀏覽:378
linux中進入ip地址伺服器 發布:2025-05-18 04:11:21 瀏覽:612
java用什麼軟體寫 發布:2025-05-18 03:56:19 瀏覽:32
linux配置vim編譯c 發布:2025-05-18 03:55:07 瀏覽:107
砸百鬼腳本 發布:2025-05-18 03:53:34 瀏覽:945
安卓手機如何拍視頻和蘋果一樣 發布:2025-05-18 03:40:47 瀏覽:742
為什麼安卓手機連不上蘋果7熱點 發布:2025-05-18 03:40:13 瀏覽:803
網卡訪問 發布:2025-05-18 03:35:04 瀏覽:511
接收和發送伺服器地址 發布:2025-05-18 03:33:48 瀏覽:372