導航:首頁 > 源碼編譯 > jvmcms標記源碼

jvmcms標記源碼

發布時間:2023-01-22 04:42:06

① 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 !

② cms如何查找標簽的所在的源碼

好像只有 米拓這個企業CMS能實現了
不過 他確實如樓主所說 是個偽開源系統
而且最惡劣的是 會收集你的信息反饋給官方
這個 很要命 誰都不希望涉及到自己的一些機密隱私信息被偷偷收集
雖然有破解版的 可是始終讓人用的不放心
連官方都安插有後門 何況是比官方還不靠譜的第三方破解組織呢~

③ JVM之ParNew和CMS日誌分析

今天這篇文章主要是對生產環境中(Java7)常用的兩種垃圾收集器(ParNew:年輕代,CMS:老年代)從日誌信息上進行分析,做一下總結,這樣當我們在排查相應的問題時,看到 GC 的日誌信息,不會再那麼陌生,能清楚地知道這些日誌是什麼意思,GC 線程當前處在哪個階段,正在做什麼事情等。

ParNew 收集器

ParNew 收集器是年輕代常用的垃圾收集器,它採用的是復制演算法,youngGC 時一個典型的日誌信息如下所示:

依次分析一下上面日誌信息的含義:

2018-04-12T13:48:26.134+0800:Mirror GC 發生的時間;

15578.050:GC 開始時,相對 JVM 啟動的相對時間,單位時秒,這里是4h+;

ParNew:收集器名稱,這里是 ParNew 收集器,它使用的是並行的 mark- 演算法,GC 過程也會 Stop the World;

3412467K->59681K:收集前後年輕代的使用情況,這里是 3.25G->58.28M;

3774912K:整個年輕代的容量,這里是 3.6G;

0.0971990 secs:Duration for the collection w/o final cleanup.

9702786K->6354533K:收集前後整個堆的使用情況,這里是 9.25G->6.06G;

24746432K:整個堆的容量,這里是 23.6G;

0.0974940 secs:ParNew 收集器標記和復制年輕代活著的對象所花費的時間(包括和老年代通信的開銷、對象晉升到老年代開銷、垃圾收集周期結束一些最後的清理對象等的花銷);

對於 [Times: user=0.95 sys=0.00, real=0.09 secs],這裡面涉及到三種時間類型,含義如下:

user:GC 線程在垃圾收集期間所使用的 CPU 總時間;

sys:系統調用或者等待系統事件花費的時間;

real:應用被暫停的時鍾時間,由於 GC 線程是多線程的,導致了 real 小於 (user+real),如果是 gc 線程是單線程的話,real 是接近於 (user+real) 時間。

CMS 收集器

CMS 收集器是老年代經常使用的收集器,它採用的是標記-清楚演算法,應用程序在發生一次 Full GC 時,典型的 GC 日誌信息如下:

CMS Full GC 拆分開來,涉及的階段比較多,下面分別來介紹各個階段的情況。

階段1:Initial Mark

這個是 CMS 兩次 stop-the-wolrd 事件的其中一次,這個階段的目標是:標記那些直接被 GC root 引用或者被年輕代存活對象所引用的所有對象,標記後示例如下所示

上述例子對應的日誌信息為:

逐行介紹上面日誌的含義:

2018-04-12T13:48:26.233+0800: 15578.148:GC 開始的時間,以及相對於 JVM 啟動的相對時間(單位是秒,這里大概是4.33h),與前面 ParNew 類似,下面的分析中就直接跳過這個了;

CMS-initial-mark:初始標記階段,它會收集所有 GC Roots 以及其直接引用的對象;

6294851K:當前老年代使用的容量,這里是 6G;

(20971520K):老年代可用的最大容量,這里是 20G;

6354687K:整個堆目前使用的容量,這里是 6.06G;

(24746432K):堆可用的容量,這里是 23.6G;

0.0466580 secs:這個階段的持續時間;

[Times: user=0.04 sys=0.00, real=0.04 secs]:與前面的類似,這里是相應 user、system and real 的時間統計。

階段2:並發標記

在這個階段 Garbage Collector 會遍歷老年代,然後標記所有存活的對象,它會根據上個階段找到的 GC Roots 遍歷查找。並發標記階段,它會與用戶的應用程序並發運行。並不是老年代所有的存活對象都會被標記,因為在標記期間用戶的程序可能會改變一些引用,如下圖所示

在上面的圖中,與階段1的圖進行對比,就會發現有一個對象的引用已經發生了變化,這個階段相應的日誌信息如下:

這里詳細對上面的日誌解釋,如下所示:

CMS-concurrent-mark:並發收集階段,這個階段會遍歷老年代,並標記所有存活的對象;

0.138/0.138 secs:這個階段的持續時間與時鍾時間;

[Times: user=1.01 sys=0.21, real=0.14 secs]:如前面所示,但是這部的時間,其實意義不大,因為它是從並發標記的開始時間開始計算,這期間因為是並發進行,不僅僅包含 GC 線程的工作。

階段3:Concurrent Preclean

Concurrent Preclean:這也是一個並發階段,與應用的線程並發運行,並不會 stop 應用的線程。在並發運行的過程中,一些對象的引用可能會發生變化,但是這種情況發生時,JVM 會將包含這個對象的區域(Card)標記為 Dirty,這也就是 Card Marking。如下圖所示

在pre-clean階段,那些能夠從 Dirty 對象到達的對象也會被標記,這個標記做完之後,dirty card 標記就會被清除了,如下

這個階段相應的日誌信息如下:

其含義為:

CMS-concurrent-preclean:Concurrent Preclean 階段,對在前面並發標記階段中引用發生變化的對象進行標記;

0.056/0.057 secs:這個階段的持續時間與時鍾時間;

[Times: user=0.20 sys=0.12, real=0.06 secs]:同並發標記階段中的含義。

階段4:Concurrent Abortable Preclean

這也是一個並發階段,但是同樣不會影響影響用戶的應用線程,這個階段是為了盡量承擔 STW(stop-the-world)中最終標記階段的工作。這個階段持續時間依賴於很多的因素,由於這個階段是在重復做很多相同的工作,直接滿足一些條件(比如:重復迭代的次數、完成的工作量或者時鍾時間等)。這個階段的日誌信息如下:

CMS-concurrent-abortable-preclean:Concurrent Abortable Preclean 階段;

3.506/3.514 secs:這個階段的持續時間與時鍾時間,本質上,這里的 gc 線程會在 STW 之前做更多的工作,通常會持續 5s 左右;

[Times: user=11.93 sys=6.77, real=3.51 secs]:同前面。

階段5:Final Remark

這是第二個 STW 階段,也是 CMS 中的最後一個,這個階段的目標是標記所有老年代所有的存活對象,由於之前的階段是並發執行的,gc 線程可能跟不上應用程序的變化,為了完成標記老年代所有存活對象的目標,STW 就非常有必要了。

通常 CMS 的 Final Remark 階段會在年輕代盡可能幹凈的時候運行,目的是為了減少連續 STW 發生的可能性(年輕代存活對象過多的話,也會導致老年代涉及的存活對象會很多)。這個階段會比前面的幾個階段更復雜一些,相關日誌如下:

對上面的日誌進行分析:

YG occupancy: 1805641 K (3774912 K):年輕代當前佔用量及容量,這里分別是 1.71G 和 3.6G;

ParNew:...:觸發了一次 young GC,這里觸發的原因是為了減少年輕代的存活對象,盡量使年輕代更干凈一些;

[Rescan (parallel) , 0.0429390 secs]:這個 Rescan 是當應用暫停的情況下完成對所有存活對象的標記,這個階段是並行處理的,這里花費了 0.0429390s;

[weak refs processing, 0.0027800 secs]:第一個子階段,它的工作是處理弱引用;

[class unloading, 0.0033120 secs]:第二個子階段,它的工作是:unloading the unused classes;

[scrub symbol table, 0.0016780 secs] ... [scrub string table, 0.0004780 secs]:最後一個子階段,它的目的是:cleaning up symbol and string tables which hold class-level metadata and internalized string respectively,時鍾的暫停也包含在這里;

6299829K(20971520K):這個階段之後,老年代的使用量與總量,這里分別是 6G 和 20G;

6348225K(24746432K):這個階段之後,堆的使用量與總量(包括年輕代,年輕代在前面發生過 GC),這里分別是 6.05G 和 23.6G;

0.1365130 secs:這個階段的持續時間;

[Times: user=1.24 sys=0.00, real=0.14 secs]:對應的時間信息。

經歷過這五個階段之後,老年代所有存活的對象都被標記過了,現在可以通過清除演算法去清理那些老年代不再使用的對象。

階段6:Concurrent Sweep

這里不需要 STW,它是與用戶的應用程序並發運行,這個階段是:清除那些不再使用的對象,回收它們的佔用空間為將來使用。如下圖所示

這個階段對應的日誌信息如下(這中間又發生了一次 Young GC):

分別介紹一下:

CMS-concurrent-sweep:這個階段主要是清除那些沒有被標記的對象,回收它們的佔用空間;

8.193/8.284 secs:這個階段的持續時間與時鍾時間;

[Times: user=30.34 sys=16.44, real=8.28 secs]:同前面;

階段7:Concurrent Reset.

這個階段也是並發執行的,它會重設 CMS 內部的數據結構,為下次的 GC 做准備,對應的日誌信息如下:

日誌詳情分別如下:

CMS-concurrent-reset:這個階段的開始,目的如前面所述;

0.044/0.044 secs:這個階段的持續時間與時鍾時間;

[Times: user=0.15 sys=0.10, real=0.04 secs]:同前面。

總結

CMS 通過將大量工作分散到並發處理階段來在減少 STW 時間,在這塊做得非常優秀,但是 CMS 也有一些其他的問題:

CMS 收集器無法處理浮動垃圾( Floating Garbage),可能出現 「Concurrnet Mode Failure」 失敗而導致另一次 Full GC 的產生,可能引發串列 Full GC;

空間碎片,導致無法分配大對象,CMS 收集器提供了一個 -XX:+UseCMSCompactAtFullCollection 開關參數(默認就是開啟的),用於在 CMS 收集器頂不住要進行 Full GC 時開啟內存碎片的合並整理過程,內存整理的過程是無法並發的,空間碎片問題沒有了,但停頓時間不得不變長;

對於堆比較大的應用上,GC 的時間難以預估。

CMS 的一些缺陷也是 G1 收集器興起的原因。

歡迎工作一到五年的Java工程師朋友們加入Java程序員開發: 854393687

群內提供免費的Java架構學習資料(裡面有高可用、高並發、高性能及分布式、Jvm性能調優、Spring源碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間「來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

④ JVM 垃圾回收( CMS 和 G1 )篇

GC Roots 對象的包括如下幾種:

當 GC 線程進行並發操作時,應用程序可能會進行新增對象、刪除對象、變更對象引用等一系列操作。這種條件下可能會出現活動對象的漏標的情況,

比如下面場景:

為了解決這個問題,還需要額外的操作,這個操作就是 write barrier。

在使用 Write bariier 之後同樣的情景就不會出現活動對象被遺漏的情況了.

YGC 時為了標記活動標記對象除了 tracing GC ROOTS 之外,老年代裡也可能會引用新生代對象。
所以正常來說還要掃描一次老年代,如果是掃描整個老年代這將會隨著堆的增大變得越來越慢,特別是現在內存都越來越大了。所以為了提升性能就引入卡表。

卡表提升性能的原理 :邏輯上把老年代內存分成一個個大小相等的卡片,然後對每個卡片准備一個與其對應的標記位,並將這些位集中起管理就好像一個表格 (mark table) 一樣,當改寫對象引用是從老年代指向新生代時,在老年代對應的卡片標記位上設置標志位即可,通常這樣的卡片我們稱之為 dirty card。

這項操作可以通過上面的提到的 write barrier 來實現,這樣就算對象跨多張卡片也不會有什麼問題。 卡表通常是用 byte 數組實現的,byte 的值只能取 [0,1] 這兩種。所以 btye [i] = 1 就表示第 i + 1 卡片所在內存上有指向新生代引用的老年代對象,這時只要 tracing 這個卡片上的對象即可。背後思想就是典型以空間換時間的思路!

G1 將整個堆劃分為一個個大小相等的小塊(每一塊稱為一個 region),每一塊的內存是連續的。和分代演算法一樣,G1 中每個塊也會充當 Eden、Survivor、Old 三種角色,但是它們不是固定的,這使得內存使用更加地靈活。

G1 收集器主要包括了以下 4 種操作:

全局並發標記過程分為五個階段

(1) 初始標記

(2) Root Region Scanning 根區域掃描

(3) Concurrent Marking 並發標記

(4) Remark 最終標記

(5) Cleanup 清除

主要完成了垃圾定位的工作,定位出了哪些分區是垃圾最多的。

並發周期結束後是混合垃圾回收周期,不僅進行年輕代垃圾收集,而且回收之前標記出來的老年代的垃圾最多的部分區塊。

MIXED GC 周期會持續進行,直到幾乎所有的被標記出來的分區(垃圾佔比大的分區)都得到回收,然後恢復到常規的年輕代垃圾收集,最終再次啟動並發周期。

下面我們來介紹特殊情況,那就是會導致 Full GC 的情況,也是我們需要極力避免的:

把 Ruser 和 Rcon 合並一下,形成一個新的,完整的可到達對象關系 Rfinal,交給 GC 程序。

CMS 和 G1 都採取一種方式 Write barrier+log,3 個步驟:

Rslog 的作用就是記錄用戶程序對對象關系的修改;
用戶程序的修改只能有 2 種:

總結: 寧可放過,下一次處理,也不錯殺

參考: CMS 收集器原理理解與分析
參考: G1 收集器原理理解與分析

⑤ 三色標記法與垃圾回收器(CMS、G1)

JVM中的CMS、G1垃圾回收器所使用垃圾回收演算法即為三色標記法。

三色標記法將對象的顏色分為了黑、灰、白,三種顏色。

存在問題:

浮動垃圾:並發標記的過程中,若一個已經被標記成黑色或者灰色的對象,突然變成了垃圾,此時,此對象不是白色的不會被清除,重新標記也不能從GC Root中去找到,所以成為了浮動垃圾,這種情況對系統的影響不大,留給下一次GC進行處理即可。
對象漏標問題(需要的對象被回收):並發標記的過程中,一個業務線程將一個未被掃描過的白色對象斷開引用成為垃圾(刪除引用),同時黑色對象引用了該對象(增加引用)(這兩部可以不分先後順序);因為黑色對象的含義為其屬性都已經被標記過了,重新標記也不會從黑色對象中去找,導致該對象被程序所需要,卻又要被GC回收,此問題會導致系統出現問題,而CMS與G1,兩種回收器在使用三色標記法時,都採取了一些措施來應對這些問題,CMS對增加引用環節進行處理(Increment Update),G1則對刪除引用環節進行處理(SATB)。

在JVM虛擬機中有兩種常見垃圾回收器使用了該演算法:

CMS(Concurrent Mark Sweep)

CMS,是非常有名的JVM垃圾回收器,它起到了承上啟下的作用,開啟了並發回收的篇章。
但是CMS由於許多小問題,現在基本已經被淘汰。

增量更新(Increment Update)
在應對漏標問題時,CMS使用了Increment Update方法來做:
在一個未被標記的對象(白色對象)被重新引用後,==引用它的對象==,若為黑色則要變成灰色,在下次二次標記時讓GC線程繼續標記它的屬性對象。
但是就算時這樣,其仍然是存在漏標的問題:

在一個灰色對象正在被一個GC線程回收時,當它已經被標記過的屬性指向了一個白色對象(垃圾)
而這個對象的屬性對象本身還未全部標記結束,則為灰色不變
而這個GC線程在標記完最後一個屬性後,認為已經將所有的屬性標記結束了,將這個灰色對象標記為黑色,被重新引用的白色對象,無法被標記

補充,CMS除了這個缺陷外,仍然存在兩個個較為致命的缺陷:

解決方案:使用Mark-Sweep-Compact演算法,減少垃圾碎片

當JVM認為內存不夠了,再使用CMS進行並發清理內存可能會發生OOM的問題,而不得不進行Serial Old GC,Serial Old是單線程垃圾回收,效率低

解決方案:降低觸發CMS GC的閾值,讓浮動垃圾不那麼容易占滿老年代

G1(Garbage First)

從G1垃圾回收器開始,G1的物理內存不再分代,而是由一塊一塊的Region組成;邏輯分代仍然存在。

前置知識 — Card Table(多種垃圾回收器均具備)

由於在進行YoungGC時,我們在進行對一個對象是否被引用的過程,需要掃描整個Old區,所以JVM設計了CardTable,將Old區分為一個一個Card,一個Card有多個對象;如果一個Card中的對象有引用指向Young區,則將其標記為Dirty Card,下次需要進行YoungGC時,只需要去掃描Dirty Card即可。

Card Table 在底層數據結構以 Bit Map實現。

CSet(Collection Set)

SATB(Snapshot At The Beginning)
在應對漏標問題時,CMS使用了SATB方法來做:

因為SATB在重新標記環節只需要去重新掃描那些被推到堆棧中的引用,並配合Rset來判斷當前對象是否被引用來進行回收;

並且在最後G1並不會選擇回收所有垃圾對象,而是根據Region的垃圾多少來判斷與預估回收價值(指回收的垃圾與回收的STW時間的一個預估值),將一個或者多個Region放到CSet中,最後將這些Region中的存活對象壓縮並復制到新的Region中,清空原來的Region。

問題:G1會不會進行Full GC?
會,當內存滿了的時候就會進行Full GC;且JDK10之前的Full GC,為單線程的,所以使用G1需要避免Full GC的產生。
解決方案:

加大內存;
提高CPU性能,加快GC回收速度,而對象增加速度趕不上回收速度,則Full GC可以避免;
降低進行Mixed GC觸發的閾值,讓Mixed GC提早發生(默認45%)

G1的第一篇paper(附錄1)發表於2004年,在2012年才在jdk1.7u4中可用。oracle官方計劃在jdk9中將G1變成默認的垃圾收集器,以替代CMS。為何oracle要極力推薦G1呢,G1有哪些優點?

首先,G1的設計原則就是簡單可行的性能調優

開發人員僅僅需要聲明以下參數即可:

其中-XX:+UseG1GC為開啟G1垃圾收集器,-Xmx32g 設計堆內存的最大內存為32G,-XX:MaxGCPauseMillis=200設置GC的最大暫停時間為200ms。如果我們需要調優,在內存大小一定的情況下,我們只需要修改最大暫停時間即可。

其次,G1將新生代,老年代的物理空間劃分取消了。

這樣我們再也不用單獨的空間對每個代進行設置了,不用擔心每個代內存是否足夠。

取而代之的是,G1演算法將堆劃分為若干個區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然採用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另外一個區域,完成了清理工作。這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內存碎片問題的存在了。

在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象佔用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1劃分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那麼G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC。

PS:在java 8中,持久代也移動到了普通的堆內存空間中,改為元空間。

對象分配策略

說起大對象的分配,我們不得不談談對象的分配策略。它分為3個階段:

對TLAB空間中無法分配的對象,JVM會嘗試在Eden空間中進行分配。如果Eden空間無法容納該對象,就只能在老年代中進行分配空間。

最後,G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的。下面我們將分別介紹一下這2種模式。

Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據為空,GC停止工作,應用線程繼續執行。

這時,我們需要考慮一個問題,如果僅僅GC 新生代對象,我們如何找到所有的根對象呢? 老年代的所有對象都是根么?那這樣掃描下來會耗費大量的時間。於是,G1引進了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個heap區內的對象引用。

在CMS中,也有RSet的概念,在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out,在進行Young GC時,掃描根時,僅僅需要掃描這一塊區域,而不需要掃描整個老年代。

但在G1中,並沒有使用point-out,這是由於一個分區太小,分區數量太多,如果是用point-out的話,會造成大量的掃描浪費,有些根本不需要GC的分區引用也掃描了。於是G1中使用point-in來解決。point-in的意思是哪些分區引用了當前分區中的對象。這樣,僅僅將這些對象當做根來掃描就避免了無效的掃描。由於新生代有多個,那麼我們需要在新生代之間記錄引用嗎?這是不必要的,原因在於每次GC時,所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。

需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,為了解決賦值器開銷這個問題,在G1 中又引入了另外一個概念,卡表(Card Table)。一個Card Table將一個分區在邏輯上劃分為固定大小的連續區域,每個區域稱之為卡。卡通常較小,介於128到512位元組之間。Card Table通常為位元組數組,由Card的索引(即數組下標)來標識每個分區的空間地址。默認情況下,每個卡都未被引用。當一個地址空間被引用時,這個地址空間對應的數組索引的值被標記為」0″,即標記為臟被引用,此外RSet也將這個數組下標記錄下來。一般情況下,這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裡面的元素是Card Table的Index。

Young GC 階段:

Mix GC不僅進行正常的新生代垃圾收集,同時也回收部分後台掃描線程標記的老年代分區。

它的GC步驟分2步:

全局並發標記(global concurrent marking)
拷貝存活對象(evacuation)
在進行Mix GC之前,會先進行global concurrent marking(全局並發標記)。 global concurrent marking的執行過程是怎樣的呢?

在G1 GC中,它主要是為Mixed GC提供標記服務的,並不是一次GC過程的一個必須環節。global concurrent marking的執行過程分為五個步驟:

初始標記(initial mark,STW)
在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) 年輕代垃圾回收密切相關。
根區域掃描(root region scan)
G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的對象。該階段與應用程序(非 STW)同時運行,並且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。
並發標記(Concurrent Marking)
G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷
最終標記(Remark,STW)
該階段是 STW 回收,幫助完成標記周期。G1 GC 清空 SATB 緩沖區,跟蹤未被訪問的存活對象,並執行引用處理。
清除垃圾(Cleanup,STW)
在這個最後階段,G1 GC 執行統計和 RSet 凈化的 STW 操作。在統計期間,G1 GC 會識別完全空閑的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閑列表時為部分並發。

提到並發標記,我們不得不了解並發標記的三色標記演算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。

根對象被置為黑色,子對象被置為灰色。

繼續由灰色遍歷,將已掃描了子對象的對象置為黑色。

遍歷了所有可達的對象後,所有可達的對象都變成了黑色。不可達的對象即為白色,需要被清理。

這看起來很美好,但是如果在標記過程中,應用程序也在運行,那麼對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題

我們看下面一種情況,當垃圾收集器掃描到下面情況時:

這時候應用程序執行了以下操作:

這樣,對象的狀態圖變成如下情形:

這時候垃圾收集器再標記掃描的時候就會下圖成這樣:

很顯然,此時C是白色,被認為是垃圾需要清理掉,顯然這是不合理的。那麼我們如何保證應用程序在運行的時候,GC標記的對象不丟失呢?有如下2中可行的方式:

在插入的時候記錄對象
在刪除的時候記錄對象
剛好這對應CMS和G1的2種不同實現方式:

在CMS採用的是增量更新(Incremental update),只要在寫屏障(write barrier)里發現要有一個白對象的引用被賦值到一個黑對象 的欄位里,那就把這個白對象變成灰色的。即插入的時候記錄下來。

在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象,它有3個步驟:

這樣,G1到現在可以知道哪些老的分區可回收垃圾最多。 當全局並發標記完成後,在某個時刻,就開始了Mix GC。這些垃圾回收被稱作「混合式」是因為他們不僅僅進行正常的新生代垃圾收集,同時也回收部分後台掃描線程標記的分區。混合式垃圾收集如下圖:

混合式GC也是採用的復制的清理策略,當GC完成後,會重新釋放空間。

至此,混合式GC告一段落了。下一小節我們講進入調優實踐。

MaxGCPauseMillis調優

前面介紹過使用GC的最基本的參數:

前面2個參數都好理解,後面這個MaxGCPauseMillis參數該怎麼配置呢?這個參數從字面的意思上看,就是允許的GC最大的暫停時間。G1盡量確保每次GC暫停的時間都在設置的MaxGCPauseMillis范圍內。 那G1是如何做到最大暫停時間的呢?這涉及到另一個概念,CSet(collection set)。它的意思是在一次垃圾收集器中被收集的區域集合。

Young GC:選定所有新生代里的region。通過控制新生代的region個數來控制young GC的開銷。
Mixed GC:選定所有新生代里的region,外加根據global concurrent marking統計得出收集收益高的若干老年代region。在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代region。
在理解了這些後,我們再設置最大暫停時間就好辦了。 首先,我們能容忍的最大暫停時間是有一個限度的,我們需要在這個限度范圍內設置。但是應該設置的值是多少呢?我們需要在吞吐量跟MaxGCPauseMillis之間做一個平衡。如果MaxGCPauseMillis設置的過小,那麼GC就會頻繁,吞吐量就會下降。如果MaxGCPauseMillis設置的過大,應用程序暫停時間就會變長。G1的默認暫停時間是200毫秒,我們可以從這里入手,調整合適的時間。

其他調優參數

避免使用以下參數:

避免使用 -Xmn 選項或 -XX:NewRatio 等其他相關選項顯式設置年輕代大小。固定年輕代的大小會覆蓋暫停時間目標。

觸發Full GC

在某些情況下,G1觸發了Full GC,這時G1會退化使用Serial收集器來完成垃圾的清理工作,它僅僅使用單線程來完成GC工作,GC暫停時間將達到秒級別的。整個應用處於假死狀態,不能處理任何請求,我們的程序當然不希望看到這些。那麼發生Full GC的情況有哪些呢?

並發模式失敗
G1啟動標記周期,但在Mix GC之前,老年代就被填滿,這時候G1會放棄標記周期。這種情形下,需要增加堆大小,或者調整周期(例如增加線程數-XX:ConcGCThreads等)。

晉升失敗或者疏散失敗
G1在進行GC的時候沒有足夠的內存供存活對象或晉升對象使用,由此觸發了Full GC。可以在日誌中看到(to-space exhausted)或者(to-space overflow)。解決這種問題的方式是:

巨型對象分配失敗
當巨型對象找不到合適的空間進行分配時,就會啟動Full GC,來釋放空間。這種情況下,應該避免分配大量的巨型對象,增加內存或者增大-XX:G1HeapRegionSize,使巨型對象不再是巨型對象。

由於篇幅有限,G1還有很多調優實踐,在此就不一一列出了,大家在平常的實踐中可以慢慢探索。最後,期待java 9能正式發布,默認使用G1為垃圾收集器的java性能會不會又提高呢?

G1處理和傳統的垃圾收集策略是不同的,關鍵的因素是它將所有的內存進行了子區域的劃分。

總結

G1是一款非常優秀的垃圾收集器,不僅適合堆內存大的應用,同時也簡化了調優的工作。通過主要的參數初始和最大堆空間、以及最大容忍的GC暫停目標,就能得到不錯的性能;同時,我們也看到G1對內存空間的浪費較高,但通過**首先收集盡可能多的垃圾(Garbage First)的設計原則,可以及時發現過期對象,從而讓內存佔用處於合理的水平。

參考鏈接:
https://juejin.cn/post/6859931488352370702
https://blog.csdn.net/qq_39276448/article/details/104470796

⑥ JVM CMS和G1執行過程比較

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。由於大部分 Java 應用主要集中在互聯網網站以及基於瀏覽器的 B/S 系統的服務端,這類應用通常會較為關注服務的響應速度,希望系統的停頓時間盡可能少,CMS 收集器就非常符合這類應用的需求

從名字可以知道,CMS 收集器是基於標記 - 清除演算法實現的,它的運作過程分為四個步驟:

由於整個過程中耗時最長的是並發標記和並發清除階段,而這兩個階段都可以和用戶線程並發執行,所以從總體上看,CMS 收集器內存回收過程是與用戶線程一起並發執行的

CMS 收集器的主要優點就是:並發收集、低停頓,因此也稱 CMS 收集器為並發低停頓收集器。但 CMS 還遠未達到完美的程度,它至少有以下四個明顯的缺點:

Garbage First(G1)收集器是一款主要面向服務端應用的垃圾收集器,開創了收集器面向局部收集的設計思路和基於 Region 的內存布局形式。HotSpot 開發團隊對 G1 收集器的期望就是能在將來替代 CMS 收集器,所以在 JDK9 發布之日,G1 便宣告取代 Parallel Scavenge 加 Parallel Old 組合,成為服務端模式下的默認垃圾收集器,而 CMS 則淪為不推薦使用

在過去,包括 CMS 在內,垃圾收集的范圍要麼是整個新生代,要麼是整個老年代,再要麼是整個 Java 堆。而 G1 可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱 CSet)進行回收,衡量標準是哪塊內存中垃圾數量最多,回收收益最大,這就是 G1 收集器的 Mixed GC 模式

雖然 G1 也是基於分代收集理論設計,但其對內存布局與其他收集器有明顯差異。G1 把連續的 Java 堆劃分成多個大小相等的獨立區域(Region),每一個 Region 可以根據需要扮演新生代的 Eden 空間、Survivor 空間、老年代空間等等。收集器能對扮演不同角色的 Region 採用不同的策略處理

Region 中還有一類特殊的 Humongous 區域,專門用來存儲大對象。只要該對象大小超過一半的 Region 的容量即可判定為大對象。而對於那些超過整個 Region 容量的超級大對象,將會被存放在 N 個連續的 Humongous Region 之中,G1 的大多數行為都把 Humongous Region 作為老年代的一部分來看待

停頓時間模型的意思是能夠支持指定在一個長度為 M 毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過 M 毫秒這么一個目標。G1 收集器作為 CMS 收集器的替代者,自然可以實現這個目標

G1 之所以能建立起可預測的停頓時間模型,是因為它將 Region 作為單詞回收的最小單元,即每次收集到的內存空間都是 Region 大小的整數倍,這樣可以有計劃地避免進行全區域的垃圾收集。G1 收集器還可以跟蹤每個 Region 的垃圾堆積的「價值」大小,即回收所獲得的空間大小以及所需時間,並在後台維護一個優先順序列表,每次根據用戶設置的允許收集停頓時間(使用 -XX:MaxGCPauseMillis 指定),優先處理回收價值最大的 Region。這種使用 Region 劃分內存空間,以及具有優先順序的區域回收方式,保證了 G1 收集器在有限的時間內獲取盡可能高的收集效率

G1 收集器的設計理念看似無太多驚人之處,其實有很多關鍵的細節問題需要解決:

G1 收集器的運作過程大致可劃分為以下四個步驟:

G1 和 CMS 都非常關注停頓時間控制,毫無疑問,可以由用戶指定期望的停頓時間是 G1 收集器的一大殺手鐧。G1 收集器經常被拿來和 CMS 收集器比較,從長遠來看,G1 收集器肯定是會取代 CMS 收集器的

除了更先進的設計理念,單從傳統的演算法理論來看,G1 從整體來看是基於標記 - 整理演算法實現,而從局部來看(兩個 Region 之間)又是基於標記 - 復制演算法實現,這意味著 G1 不會產生內存碎片。但 G1 並非全方面碾壓 CMS,G1 由於其復雜的內部細節實現,使得垃圾收集時的內存佔用和程序運行時的額外執行負載都要比 CMS 高。使用哪款收集器,往往要針對具體場景才能做定量比較,目前在小內存應用上 CMS 的表現大概率會優於 G1,而在大內存應用上 G1 則佔有優勢,這個平衡點通常在 6GB ~ 8GB 之間。當然,隨著 HotSpot 開發者對 G1 的持續優化,最終勝利的天平必定迴向 G1 傾斜

⑦ 垃圾收集器-CMS、三色標記、記憶集

    CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。它非常符合在注重用戶體驗的應用上使用,它是HotSpot虛擬機第一款真正意義上的並發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。

    從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 「標記-清除」演算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加復雜一些。整個過程分為四個步驟:

初始標記:

    暫停所有的其他線程(STW),並記錄下gc roots直接能引用的對象,速度很快

並發標記:

    並發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程, 這個過程耗時較長但是不需要停頓用戶線程, 可以與垃圾收集線程一起並發運行。因為用戶程序繼續運行,可能會有導致已經標記過的對象狀態發生改變。

重新標記:

    重新標記階段就是為了修正並發標記期間因為用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短。主要用到三色標記里的增量更新演算法(見下面詳解)做重新標記。

並發清理:

    開啟用戶線程,同時GC線程開始對未標記的區域做清掃。這個階段如果有新增對象會被標記為黑色不做任何處理(見下面三色標記演算法詳解)。

並發重置:

    重置本次GC過程中的標記數據。

從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:並發收集、低停頓。但是它有下面幾個明顯的缺點:

1.對CPU資源敏感(會和服務搶資源);

2.無法處理浮動垃圾( 在並發標記和並發清理階段又產生垃圾 ,這種浮動垃圾只能等到下一次gc再清理了);

3.它使用的回收演算法-「標記-清除」演算法會導致收集結束時會有大量空間碎片產生,當然通過參數-XX:+UseCMSCompactAtFullCollection可以讓jvm在執行完標記清除後再做整理執行過程中的不確定性,會存在上一次垃圾回收還沒執行完,然後垃圾回收又被觸發的情況,特別是 在並發標記和並發清理階段會出現,一邊回收,系統一邊運行,也許沒回收完就再次觸發full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收

CMS的相關核心參數

1.-XX:+UseConcMarkSweepGC:啟用cms

2.-XX:ConcGCThreads:並發的GC線程數

3.-XX:+UseCMSCompactAtFullCollection:FullGC之後做壓縮整理(減少碎片)

4.-XX:CMSFullGCsBeforeCompaction:多少次FullGC之後壓縮一次,默認是0,代表每次FullGC後都會壓縮一 次

5.-XX:: 當老年代使用達到該比例時會觸發FullGC(默認是92,這是百分比)

6.-XX:+UseCMSInitiatingOccupancyOnly:只使用設定的回收閾值(-XX:設 定的值),如果不指定,JVM僅在第一次使用設定值,後續則會自動調整

7.-XX:+CMSScavengeBeforeRemark:在CMS GC前啟動一次minor gc,目的在於減少老年代對年輕代的引用,降低CMS GC的標記階段時的開銷,一般CMS的GC耗時 80%都在標記階段

8.-XX:+CMSParallellnitialMarkEnabled:表示在初始標記的時候多線程執行,縮短STW

9.-XX:+CMSParallelRemarkEnabled:在重新標記的時候多線程執行,縮短STW;

    在並發標記的過程中,因為標記期間應用線程還在繼續跑,對象間的引用可能發生變化,多標和漏標的情況就有可能發生。這里引入「三色標記」來給大家解釋下,把Gcroots可達性分析遍歷對象過程中遇到的對象, 按照「是否訪問過」這個條件標記成以下三種顏色:

黑色:

    表示對象已經被垃圾收集器訪問過, 且這個對象的所有引用都已經掃描過。 黑色的對象代表已經掃描過, 它是安全存活的, 如果有其他對象引用指向了黑色對象, 無須重新掃描一遍。 黑色對象不可能直接(不經過灰色對象) 指向某個白色對象。

灰色:

    表示對象已經被垃圾收集器訪問過, 但這個對象上至少存在一個引用還沒有被掃描過。

白色:

    表示對象尚未被垃圾收集器訪問過。 顯然在可達性分析剛剛開始的階段, 所有的對象都是白色的, 若在分析結束的階段, 仍然是白色的對象, 即代表不可達。

標記過程:

初始時,所有對象都在 【白色集合】中;

將GC Roots 直接引用到的對象 挪到 【灰色集合】中;

從灰色集合中獲取對象:

3.1. 將本對象 引用到的 其他對象 全部挪到 【灰色集合】中;

3.2. 將本對象 挪到 【黑色集合】裡面。

重復步驟3,直至【灰色集合】為空時結束。

結束後,仍在【白色集合】的對象即為GC Roots 不可達,可以進行回收

多標-浮動垃圾

    在並發標記過程中,如果由於方法運行結束導致部分局部變數(gcroot)被銷毀,這個gcroot引用的對象之前又被掃描過 (被標記為非垃圾對象),那麼本輪GC不會回收這部分內存。這部分本應該回收但是沒有回收到的內存,被稱之為「浮動 垃圾」。浮動垃圾並不會影響垃圾回收的正確性,只是需要等到下一輪垃圾回收中才被清除。

    另外,針對並發標記(還有並發清理)開始後產生的新對象,通常的做法是直接全部當成黑色,本輪不會進行清除。這部分 對象期間可能也會變為垃圾,這也算是浮動垃圾的一部分。

漏標-讀寫屏障

漏標只有 同時滿足 以下兩個條件時才會發生:

     條件一:灰色對象 斷開了 白色對象的引用;即灰色對象 原來成員變數的引用 發生了變化。

    條件二:黑色對象 重新引用了 該白色對象;即黑色對象 成員變數增加了 新的引用。

漏標會導致被引用的對象被當成垃圾誤刪除,這是嚴重bug,必須解決,有兩種解決方案:  增量更新(Incremental Update) 和原始快照(Snapshot At The Beginning,SATB)  。

增量更新 就是當黑色對象 插入新的指向 白色對象的引用關系時, 就將這個新插入的引用記錄下來, 等並發掃描結束之後, 再將這些記錄過的引用關系中的黑色對象為根, 重新掃描一次。 這可以簡化理解為, 黑色對象一旦新插入了指向白色對象的引用之後, 它就變回灰色對象了。

原始快照 就是當灰色對象要 刪除指向 白色對象的引用關系時, 就將這個要刪除的引用記錄下來, 在並發掃描結束之後, 再將這些記錄過的引用關系中的灰色對象為根, 重新掃描一次,這樣就能掃描到白色的對象,將白色對象直接標記為黑色(目的就是讓這種對象在本輪gc清理中能存活下來,待下一輪gc的時候重新掃描,這個對象也有可能是浮動垃圾)

以上 無論是對引用關系記錄的插入還是刪除, 虛擬機的記錄操作都是通過寫屏障實現的。

寫屏障實現原始快照(SATB): 當對象B的成員變數的引用發生變化時,比如引用消失(a.b.d = null),我們可以利用寫屏障,將B原來成員變數的引用對象D記錄下來:

寫屏障實現增量更新: 當對象A的成員變數的引用發生變化時,比如新增引用(a.d = d),我們可以利用寫屏障,將A新的成員變數引用對象D 記錄下來:

記憶集

當我們進行young gc時,我們的 gc roots除了常見的棧引用、靜態變數、常量、鎖對象、class對象 這些常見的之外,如果 老年代有對象引用了我們的新生代對象 ,那麼老年代的對象也應該加入gc roots的范圍中,但是如果每次進行young gc我們都需要掃描一次老年代的話,那我們進行垃圾回收的代價實在是太大了,因此我們引入了一種叫做記憶集的抽象數據結構來記錄這種引用關系。

什麼是記憶集?

    記憶集是一種用於記錄從非收集區域指向收集區域的指針集合的數據結構。

    如果我們不考慮效率和成本問題,我們可以用一個數組存儲所有有指針指向新生代的老年代對象。但是如果這樣的話我們維護成本就很好,打個比方,假如所有的老年代對象都有指針指向了新生代,那麼我們需要維護整個老年代大小的記憶集,毫無疑問這種方法是不可取的。因此我們引入了卡表的數據結構

什麼是卡表?

    記憶集是我們針對於跨代引用問題提出的思想,而卡表則是針對於該種思想的具體實現。(可以理解為記憶集是結構,卡表是實現類)

    在hotspot虛擬機中,卡表是一個位元組數組,數組的每一項對應著內存中的某一塊連續地址的區域,如果該區域中有引用指向了待回收區域的對象,卡表數組對應的元素將被置為1,沒有則置為0;

G1的記憶集

上述的 卡表機制基本上適用於CMS垃圾回收器 ,因為CMS垃圾回收器只需要在young gc時維護老年代對新生代的引用即可,但是G1垃圾回收器不一樣,因為G1垃圾回收器是基於分區模型的,所以每一個Region需要知道有哪些region的引用指向了它,並且這些region是不是本次垃圾回收區域的一部分。因此G1垃圾回收器不能簡單的只維護一個卡表(卡表只能簡單的知道某塊內存區域有沒有引用收集區域的對象,但是不能知道到底是誰引用了自己),所以在 G1垃圾回收器的記憶集的實現實際上是基於哈希表的 ,key代表的是其他region的起始地址,value是一集合,裡面存放了對應區域的卡表的索引,因此G1的region能夠通過記憶集知道,當前是哪個region有引用指向了它,並且能知道是哪塊區域存在指針指向。

但是大家應該能注意到, 每個region都維護一個記憶集,內存佔用量肯定很大,這也就是為什麼G1垃圾回收器比傳統的其他垃圾回收器要有更高的內存佔用 。據統計G1至少要耗費大約10%-20%的Java堆空間來維護收集器的工作。

參考:

https://blog.csdn.net/xc1989xc/article/details/107466313

https://blog.csdn.net/shangshanzixu/article/details/113918994

⑧ JVM垃圾回收的「三色標記演算法」實現,內容太干

三色標記法是一種垃圾回收法,它可以讓JVM不發生或僅短時間發生STW(Stop The World),從而達到清除JVM內存垃圾的目的。JVM中的 CMS、G1垃圾回收器 所使用垃圾回收演算法即為三色標記法。

三色標記法將對象的顏色分為了黑、灰、白,三種顏色。

白色 :該對象沒有被標記過。(對象垃圾)

灰色 :該對象已經被標記過了,但該對象下的屬性沒有全被標記完。(GC需要從此對象中去尋找垃圾)

黑色 :該對象已經被標記過了,且該對象下的屬性也全部都被標記過了。(程序所需要的對象)

從我們main方法的根對象(JVM中稱為GC Root)開始沿著他們的對象向下查找,用黑灰白的規則,標記出所有跟GC Root相連接的對象,掃描一遍結束後,一般需要進行一次短暫的STW(Stop The World),再次進行掃描,此時因為黑色對象的屬性都也已經被標記過了,所以只需找出灰色對象並順著繼續往下標記(且因為大部分的標記工作已經在第一次並發的時候發生了,所以灰色對象數量會很少,標記時間也會短很多), 此時程序繼續執行,GC線程掃描所有的內存,找出掃描之後依舊被標記為白色的對象(垃圾),清除。

具體流程:

在JVM虛擬機中有兩種常見垃圾回收器使用了該演算法:CMS(Concurrent Mark Sweep)、G1(Garbage First) ,為了解決三色標記法對對象漏標問題各自有各自的法:

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在互聯網網站或者基於瀏覽器的B/S系統的服務端上,這類應用通常都會較為關注服務的響應速度,希望系統停頓時間盡可能短,以給用戶帶來良好的交互體驗。CMS收集器就非常符合這類應用的需求(但是實際由於某些問題,很少有使用CMS作為主要垃圾回收器的)。

從名字(包含「Mark Sweep」)上就可以看出CMS收集器是基於標記-清除演算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為四個步驟,包括:1)初始標記(CMS initial mark) 2)並發標記(CMS concurrent mark) 3)重新標記(CMS remark) 4)並發清除(CMS concurrent sweep)

其中初始標記、重新標記這兩個步驟仍然需要「Stop The World」。初始標記僅僅只是標記一下GCRoots能直接關聯到的對象,速度很快;

並發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起並發運行;

重新標記階段則是為了修正並發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短;

最後是並發清除階段,清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時並發的。由於在整個過程中耗時最長的並發標記和並發清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內存回收過程是與用戶線程一起並發執行的。

在應對漏標問題時,CMS使用了增量更新(Increment Update)方法來做:

在一個未被標記的對象(白色對象)被重新引用後, 引用它的對象若為黑色則要變成灰色,在下次二次標記時讓GC線程繼續標記它的屬性對象

但是就算是這樣,其仍然是存在漏標的問題:

G1(Garbage First)物理內存不再分代,而是由一塊一塊的Region組成,但是邏輯分代仍然存在。G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。

Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為只要大小超過了一個Region容量一半的對象即可判定為大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待,如圖所示

Card Table(多種垃圾回收器均具備)

RSet(Remembered Set)

是輔助GC過程的一種結構,典型的空間換時間工具,和Card Table有些類似。

後面說到的CSet(Collection Set)也是輔助GC的,它記錄了GC要收集的Region集合,集合里的Region可以是任意年代的。

在GC的時候,對於old->young和old->old的跨代對象引用,只要掃描對應的CSet中的RSet即可。邏輯上說每個Region都有一個RSet,RSet記錄了其他Region中的對象引用本Region中對象的關系,屬於points-into結構(誰引用了我的對象)。

而Card Table則是一種points-out(我引用了誰的對象)的結構,每個Card 覆蓋一定范圍的Heap(一般為512Bytes)。G1的RSet是在Card Table的基礎上實現的:每個Region會記錄下別的Region有指向自己的指針,並標記這些指針分別在哪些Card的范圍內。這個RSet其實是一個Hash Table,Key是別的Region的起始地址,Value是一個集合,裡面的元素是Card Table的Index。每個Region中都有一個RSet,記錄其他Region到本Region的引用信息;使得垃圾回收器不需要掃描整個堆找到誰引用當前分區中的對象,只需要掃描RSet即可。

CSet(Collection Set)

一組可被回收的分區Region的集合, 是多個對象的集合內存區域。

新生代與老年代的比例

5% - 60%,一般不使用手工指定,因為這是G1預測停頓時間的基準,這地方簡要說明一下,G1可以指定一個預期的停頓時間,然後G1會根據你設定的時間來動態調整年輕代的比例,例如時間長,就將年輕代比例調小,讓YGC盡早行。

SATB(Snapshot At The Beginning), 在應對漏標問題時,G1使用了SATB方法來做,具體流程:

因為SATB在重新標記環節只需要去重新掃描那些被推到堆棧中的引用,並配合Rset來判斷當前對象是否被引用來進行回收;

並且在最後G1並不會選擇回收所有垃圾對象,而是根據Region的垃圾多少來判斷與預估回收價值(指回收的垃圾與回收的STW時間的一個預估值),將一個或者多個Region放到CSet中,最後將這些Region中的存活對象壓縮並復制到新的Region中,清空原來的Region。

會,當內存滿了的時候就會進行Full GC;且JDK10之前的Full GC,為單線程的,所以使用G1需要避免Full GC的產生。

解決方案:

閱讀全文

與jvmcms標記源碼相關的資料

熱點內容
縱向加密密鑰協商狀態時間 瀏覽:850
mc花雨庭伺服器有些什麼 瀏覽:809
linux製作網頁 瀏覽:19
xlsx加密忘記了怎麼辦 瀏覽:999
app湖北農信怎麼解約 瀏覽:426
在線編程教育項目 瀏覽:759
電信采購5萬台伺服器干什麼用 瀏覽:200
騰訊雲伺服器登錄地址 瀏覽:987
程序員在地鐵上寫字 瀏覽:555
解壓包未知文件格式怎麼辦 瀏覽:576
程序員破壞資料庫 瀏覽:331
sh格式如何編譯 瀏覽:344
虛擬伺服器雲主機哪個好 瀏覽:98
單片機埠保護 瀏覽:948
iso壓縮gho 瀏覽:14
網關熔斷器演算法 瀏覽:629
不銹鋼高度演算法 瀏覽:170
基於單片機的畢業設計論文 瀏覽:658
久佳跑步機的app怎麼下載 瀏覽:201
python列印心形 瀏覽:48