《Java 底層原理》Jvm GC算法

前言

之前的學習也是爲了讓我們更好的理解GC,GC是我們學習Jvm的核心,因爲我們後面的優化,爲什麼會出現oom,怎麼調整堆空間的大小等等。

GC算法

第一種標記算法:引用計數法

在對象中添加一個屬性用於標記對象被引用的次數,每多一個其他對象引用,計數+1,當引用失效時,計數-1,如果計數=0,表示沒有其他對象引用,就可以被回收。

這個算法無法解決循環依賴的問題。比如A,B對象相互引用,這樣就會計數增加,不會出現計數減少。

第二種標記算法:可達性算法

通過一系列被稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係鏈向下搜索,如果某個對象無法被搜索到,則說明該對象無引用執行,可回收。相反,則對象處於存活狀態,不可回收。JVM中的實現是找到存活對象,未打標記的就是無用對象,GC時會回收。

哪些對象可以作爲GC Root呢:

  • 所有Java線程當前活躍的棧幀裏指向GC堆裏的對象的引用;換句話說,當前所有正在被調用的方法的引用類型的參數/局部變量/臨時值。
  • VM的一些靜態數據結構裏指向GC堆裏的對象的引用,例如說HotSpot VM裏的Universe裏有很多這樣的引用。
  • JNI handles,包括global handles和local handles
  • (看情況)所有當前被加載的Java類
  • (看情況)Java類的引用類型靜態變量
  • (看情況)Java類的運行時常量池裏的引用類型常量(String或Class類型)
  • (看情況)String常量池(StringTable)裏的引用

Jvm是如何找存活對象的?

3色標記算法

未發生gc時,所有的對象視爲白色。

發生gc後,新創建的對象全部視爲黑色。

  • 白色:尚未訪問過。
  • 本對象已訪問過,而且本對象引用到的其他對象也全部訪問過了。
  • 本對象已訪問過,但是本對象引用到的其他對象尚未全部訪問完。全部訪問後,會轉換爲黑色。

從3色標記的說明可以很容易的看出,最後白色未被訪問的對象是需要回收的對象。

併發的情況下:3色標記會存在對象被多標記,少標記,漏標記的情況。

多標的情況:因爲GC線程和用戶併發執行,所以當GC線程剛剛標記上,而用戶線程緊接着就斷開引用了的情況。

少標的情況:用戶線程新創建的對象,默認是黑色的,可以躲過本次GC,下次GC可以被掃描到。

漏標的情況:一個完成黑色標記的對象,指向了一個白色對象,這個時候就產生的漏標。

  • 有至少一個黑色對象在自己被標記之後指向了這個白色對象
  • 所有的灰色對象在自己引用掃描完成之前刪除了對白色對象的引用

多標,少標問題不大,下次GC都可以處理,但是漏標會導致程序報錯,這個必須要處理。處理方式有兩種:

增量更新:GC過程中發送的引用關係變化都記錄下來,等GC標記完成後,再掃描一下記錄的對象的引用情況即可。

原始快照:當一個灰色對象去掉對白色對象的引用,這種情況下,這個引用關係會被記錄下來。 讓這個白色對象變成灰色,後面繼續掃描,會產生一點垃圾對象。

第一種GC算法:標記清除算法

沒有被打上標記的對象,被清除掉。

第二種GC算法:標記清除-整理算法

第一步:沒有被打上標記的對象,被清除掉。

第二步:將內存碎片整理合並。

第三種GC算法:分代+複製算法

Eden區滿了觸發GC,

第一步:Eden區加From區的對象,進行標記,還被引用的對象打上標記。

第二步:將已經被標記的對象,存放到To區。清空From區和Eden區的對象。

後面的FC 也是一樣的道理,只是From 區和To區的位置交換而已。

詳細的Jvm,內存模型見:https://www.cnblogs.com/jssj/p/14290060.html

這裏有一個重點知識:存活的對象被遷移到新的內存空間,這就牽涉到對象內存地址的變更即對象搬家了,如何保證還是可以讓程序正常使用?

Jvm 使用了動態計算指針來實現的,引用指針會指向新的內存地址,內存是連續的,可以通過計算內存移動的距離和自己的長度計算新內存地址的位置。

內存分配算法

指針碰撞:自旋方式獲取內存空間。

空閒隊列:將內存先劃分cell小塊,再進行分配。

GC中還有兩個概念,安全點和安全區。

垃圾收集器

名詞STW:Stop The World。即GC線程與用戶線程無法併發運行,GC線程執行期間需要暫停用戶線程

Jvm有10種垃圾回收器。

java -XX:+PrintCommandLineFlags -version   -- windows 使用該命令可以查看默認使用的垃圾回收器

標紅的就是。

1、Serial收集器

串行垃圾收集器,即GC線程與用戶線程先後運行,即GC時需要STW(暫停所有用戶線程),直至GC結束才恢復用戶線程的運行專注於收集年輕代,底層是複製算法

相關參數:-XX:+UseSerialGC

2.Serial Old收集器

Serial Old 和Serial 相比就是應用在老年代的垃圾收集器,也是單線程,但是算法不是copy,而是Mark-Compact 標記整理算法,也是stw:暫停所有線程進行垃圾回收;

所以 Serial 和Serial Old 組合使用,可用內存一般不大(幾十M至一兩百M)的服務器環境中,不適合當前的大內存了

3.ParNew收集器

ParNew垃圾收集器是Serial收集器的改進多線程版本(因爲內存的不斷增大),除了多線程外,其餘的行爲、特點和Serial收集器一樣,實現算法跟Serial完全一樣(copy算法),也是stw下執行;

但是如果CPU數量爲1個或者少於4個時,該種收集器的性能並不會比Serial要好。因爲除去上下文切換,以及佔用用戶線程CPU時間片,導致用戶線程被拖慢

在Server模式下,ParNew收集器是一個非常重要的收集器,因爲除Serial外,目前只有它能與CMS收集器配合工作;CMS是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;

1 ) CMS作爲老年代收集器,但卻無法與JDK1.4已經存在的新生代收集器Parallel Scavenge配合工作;

2) 因爲Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼框架,而另外獨立實現;而其餘幾種收集器則共用了部分的框架代碼;

設置參數

“-XX:+UseConcMarkSweepGC”:指定使用CMS後,會默認使用ParNew作爲新生代收集器;
“-XX:+UseParNewGC”:強制指定使用ParNew;
“-XX:ParallelGCThreads”:指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量

4.Parallel Old

這個是Serial Old的多線程版本,應用在老年代的收集器,也是標記-整理算法,並且是stw的執行收集。但是如果CPU數量少的話性能一樣不好。但是現在無論是PC還是server CPU數量都不再是性能瓶頸限制了,所以目前它跟Parallel Scavenge的配合是吞吐量優先場景的優先收集器選擇。

5.Parallel Scavenge

一種新生代垃圾收集器,與 ParNew相比不可以與cms一起組合使用,PS也是複製算法,它與前兩種收集器最大的區別是,它關注的是吞吐量而不是延遲。也被稱爲是吞吐量優先的收集器。其中,吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。

主要使用場景:主要適合在後臺運算而不是太多交互的任務,高吞吐量則可以最高效率的利用CPU時間,儘快的完成程序的運算任務。當然,如果想要降低停頓時間,相應的也會影響吞吐量

6.CMS
CMS,Concurrent Mark Sweep,這是一款真正的併發收集器,就是在線程執行過程中也可以進行垃圾收集的收集器,在一些對響應時間有很高要求的應用或網站中,用戶程序不能有長時間的停頓,CMS 可以用於此場景。 分爲四個過程 ,1初始標記 ,2併發標記,3重新標記,4併發清理

CMS採用了多種方式儘可能降低GC的暫停時間,減少用戶程序停頓。停頓時間降低的同時犧牲了CPU吞吐量 。

因爲併發情況佔用大量cpu資源,這是在停頓時間和性能間做出的取捨,可以簡單理解爲"空間(性能)"換時間,CMS 是一個承前啓後的收集器

7.G1(Garbage First)

在JDK7就已加入JVM的收集器大家庭中,成爲HotSpot重點發展的垃圾回收技術。同優秀的CMS垃圾回收器一樣,G1也是關注最小時延的垃圾回收器,也同樣適合大尺寸堆內存的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1最大的特點是引入分區的思路,弱化了分代的概念,合理利用垃圾收集各個週期的資源,解決了其他收集器甚至CMS的衆多缺陷
G1收集器,是比前面的更優秀,真正有突破的一款垃圾收集器。其實在G1中還是保留了分代的概念,但是實際上已經在新生代和老年代中沒有物理隔離了。在G1中,內存空間被分割成一個個的Region區,所謂新生代和老年代,都是由一個個region組成的。同時G1也不需要跟別的收集器一起配合使用,自己就可以搞定所有內存區域。整體上來講不是一個分代收集器,是一個通喫收集器。這也是JVM內存管理和垃圾收集的一個發展趨勢。從後面zgc中我們可以更清晰的看到這個變化。

G1採用了標記-整理算法,避免了CMS中的內存碎片問題,另外它能達到可控的垃圾時間。是一款優秀的收集器。即便如此,從2004年第一篇論文發表到真正商用推出,也是到了jdk1.7。實現上並不是那麼容易的。

G1的工作過程:

初始標記:這個過程跟CMS第一個過程差不多,只是標記一下GC Root關聯的對象。

併發標記:這個過程時間比較久,分析GC Root到所有對象的可達性分析。如果從GC Root節點開始遍歷所有對象會比較耗時,實際上JVM也不是這麼做的。JVM是使用Remembered Set保存了對象引用的調用信息,在可達性分析的時候只需要同時遍歷remembered set就好了,不需要從根節點開始挨個遍歷。

最終標記:由於併發標記階段,用戶線程仍然在工作,會對標記產生一些偏差,這時候需要通過remembered set log來記錄這些改變,在這個階段將改變合併到remembered set中。完成最終標記。

篩選清除:通過標記整理的算法,根據用戶配置的回收時間,和維護的優先級列表,優先收集價值最大的region。收集階段是基於標記-整理和複製算法實現

記憶集和卡表

 

8.ZGC

zgc是jdk11中要發佈的最新垃圾收集器。完全沒有分代的概念,先說下它的優點吧,官方給出的是無碎片,時間可控,超大堆。

9.Shenandoah

Shenandoah是一款concurrent及parallel的垃圾收集器;跟ZGC一樣也是面向low-pause-time的垃圾收集器,不過ZGC是基於colored pointers來實現,而Shenandoah GC是基於brooks pointers來實現。

其實低停頓的GC,業界早就出現,只不過Java比較晚

Azul的Zing中C4 GC 土豪選擇

oracle中的HotSpot ZGC JDK11的選擇

R大說ZGC說抄襲Azul的,兩者是等價的。

10.Epsilon

java 11 新的Epsilon垃圾收集器

Epsilon(A No-Op Garbage Collector)垃圾回收器控制內存分配,但是不執行任何垃圾回收工作。一旦java的堆被耗盡,jvm就直接關閉。設計的目的是提供一個完全消極的GC實現,分配有限的內存分配,最大限度降低消費內存佔用量和內存吞吐時的延遲時間。一個好的實現是隔離代碼變化,不影響其他GC,最小限度的改變其他的JVM代碼。

總結

gc 是Jvm非常重要的一環,可以說是最核心的,讓代碼使用者可以不關心內存分配的問題,作爲Java開發者,是必須掌握的核心技術。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章