十七、垃圾回收器

 

1、GC分類與性能指標

 

  • 垃圾收集器沒有在規範中進行過多的規定,可以由不同的廠商、不同版本的JVM來實現。
  • 由於JDK的版本處於高速迭代過程中,因此Java發展至今已經衍生了衆多的GC版本。
  • 從不同角度分析垃圾收集器,可以將GC分爲不同的類型。

 

 

按線程數分,可以分爲串行垃圾回收器和並行垃圾回收器。

 

  • 串行回收指的是在同一時間段內只允許有一個CPU用於執行垃圾回收操作,此時工作線程被暫停,直至垃圾收集工作結束。

> 在諸如單CPU處理器或者較小的應用內存等硬件平臺不是特別優越的場合,串行回收器的性能表現可以超過並行回收器和併發回收器。所以,串行回收默認被應用在客戶端的client模式下的JVM中

> 在併發能力比較強的CPU上,並行回收器產生的停頓時間要短於串行回收器。

  • 和串行回收相反,並行收集可以運用多個CPU同時執行垃圾回收,因此提升了應用的吞吐量,不過並行回收仍然與串行回收一樣,採用獨佔式,使用了"Stop-the-world"機制。

 

 

按照工作模式分,可以分爲併發式垃圾回收器和獨佔式垃圾回收器

> 併發式垃圾回收器與應用程序線程交替工作,以儘可能減少應用程序的停頓時間。

> 獨佔式垃圾回收器(Stop the world)一旦運行,就停止應用程序中的所有用戶線程,直到垃圾回收過程完全結束。

 

 

按碎片處理方式分,可分爲壓縮式垃圾回收器和非壓縮式垃圾回收器。

> 壓縮式垃圾回收器會在回收完成後,對存活對象進行壓縮整理,消除回收後的碎片。

再分配對象空間使用:指針碰撞

> 非壓縮式的垃圾回收器不進行這步操作。

再分配對象空間使用:空閒列表

按工作的內存區間分,又可分爲年輕代垃圾回收器和老年代垃圾回收器。

 

 

 

評估GC的性能指標

 

  • 吞吐量:運行用戶代碼的時間佔總運行時間的比例

> (總運行時間:程序的運行時間+內存回收的時間)

  • 垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總運行時間的比例。
  • 暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間。
  • 收集頻率:相對於應用程序的執行,收集操作發生的頻率。
  • 內存佔用: Java堆區所佔的內存大小。
  • 快速:一個對象從誕生到被回收所經歷的時間。

 

 

  • 這三者共同構成一個“不可能三角”。三者總體的表現會隨着技術進步而越來越好。 一款優秀的收集器通常最多同時滿足其中的兩項。
  • 這三項裏,暫停時間的重要性日益凸顯。因爲隨着硬件發展,內存佔用多些越來越能容忍,硬件性能的提升也有助於降低收集器運行時對應用程序的影響,即提高了吞吐量。而內存的擴大,對延遲反而帶來負面效果。
  • 簡單來說,主要抓住兩點

> 吞吐量

> 暫停時間

 

 

評估GC的性能指標:吞吐量(throughput)

  • 吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/ (運行用戶代碼時間+垃圾收集時間) 。

> 比如:虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%

  • 這種情況下,應用程序能容忍較高的暫停時間,因此,高吞吐量的應用程序有更長的時間基準,快速響應是不必考慮的。
  • 吞吐量優先,意味着在單位時間內, STW的時間最短: 0.2 +0.2 =0.4

 

評估GC的性能指標:暫停時間(pause time)

 

  • “暫停時間”是指一個時間段內應用程序線程暫停,讓GC線程執行的狀態

> 例如, GC期間100毫秒的暫停時間意味着在這100毫秒期間內沒有應用程序線程是活動的。

  • 暫停時間優先,意味着儘可能讓單次STW的時間最短: 0.1 +0.1+0.1+0.1 + 0.1= 0.5

 

 

 

評估GC的性能指標:吞吐量vs暫停時間

 

  • 高吞吐量較好因爲這會讓應用程序的最終用戶感覺只有應用程序線程在做“生產性”工作。直覺上,吞吐量越高程序運行越快。
  • 低暫停時間(低延遲)較好因爲從最終用戶的角度來看不管是GC還是其他原因導致一個應用被掛起始終是不好的。這取決於應用程序的類型,有時候甚至短暫的200毫秒暫停都可能打斷終端用戶體驗。因此,具有低的較大暫停時間是非常重要的,特別是對於一個交互式應用程序。
  • 不幸的是”高吞吐量”和”低暫停時間”是一對相互競爭的目標(矛盾)。

> 因爲如果選擇以吞吐量優先,那麼必然需要降低內存回收的執行頻率,但是這樣會導致GC需要更長的暫停時間來執行內存回收。

> 相反的,如果選擇以低延遲優先爲原則,那麼爲了降低每次執行內存回收時的替停時間,也只能頻繁地執行內存回收,但這又引起了年輕代內存的縮減和導致程序吞吐量的下降。

 

 

在設計(或使用) GC算法時,我們必須確定我們的目標:一個GC算法只可能針對兩個目標之一(即只專注於較大吞吐量或最小暫停時間) ,或嘗試找到一個二者的折衷。

現在標準:在最大吞吐量優先的情況下,降低停頓時間。

 

 

2、不同的垃圾回收器概述

 

垃圾收集機制是Java的招牌能力,極大地提高了開發效率。這當然也是面試的熱點。

那麼, Java常見的垃圾收集器有哪些?

 

 

垃圾收集器發展史

有了虛擬機,就一定需要收集垃圾的機制,這就是Garbage Collection,對應的產品我們稱爲Garbage Collector.

  • 1999年隨JDK1.3.1一起來的是串行方式的Serial GC ,它是第一款GC. ParNew垃圾收集器是Serial收集器的多線程版本
  • 2002年2月26日, Parallel GC和Concurrent Mark Sweep GC跟隨JDK1.4.2一起發佈
  • Parallel GC在JDK6之後成爲HotSpot默認GC.
  • 2012年,在JDK1.7u4版本中, G1可用
  • 2017年, JDK9中G1變成默認的垃圾收集器,以替代CMS.
  • 2018年3月, JDK10中G1垃圾回收器的並行完整垃圾回收,實現並行性來改善最壞情況下的延遲
  • 2018年9月, JDK11發佈。引入Epsilon垃圾回收器,又被稱爲"No-Op (無操作) "回收器。同時,引入ZGC:可伸縮的低延遲垃圾回收器(Experimental)。
  • 2019年3月, JDK12發佈。增強G1, 自動返回未用堆內存給操作系統。同時,引入

Shenandoah GC:低停頓時間的GC (Experimental)。

  • 2019年9月, JDK13發佈。增強ZGC, 自動返回未用堆內存給操作系統。
  • 2020年3月, JDK14發佈。刪除CMS垃圾回收器。擴展ZGC在macOS和Windows上的應用

 

 

 

7款經典的垃圾收集器

  • 串行回收器: Serial, Serial Old
  • 並行回收器: ParNew , Parallel Scavenge, Parallel Old
  • 併發回收器: CMS, G1

 

 

https://www.oracle.com/technetwork/java/javase/tech/memorymanagement-whitepaper-1-150020.pdf

 

 

7款經典收集器與垃圾分代之間的關係

 

新生代收集器: Serial 、 ParNew 、 Parallel Scavenge;

老年代收集器: Serial Old 、 Parallel Old 、CMS;

整堆收集器: G1;

 

垃圾收集器的組合關係

 

 

1,兩個收集器間有連線,表明它們可以搭配使用:

Serial/Serial Old, Serial/CMS, ParNew/Serial Old, ParNew/CMS

Parallel Scavenge/Serial Old, Parallel Scavenge/Parallel Old, G1;

2,其中Serial Old作爲CMS出現"Concurrent Mode Failure"失敗的後備預案。

3, (紅色虛線)由於維護和兼容性測試的成本,在JDK 8時將Serial+CMS

ParNew+Serial Old 這兩個組合聲明爲廢棄(JEP 173) ,並在JDK 9中完全取消了這些組合的支持(JEP214) ,即:移除。

4, (綠色虛線)JDK 14中:棄用Parallel Scavenge和SerialOld GC組合 (JEP366)

5, (青色虛線)JDK 14中:刪除CMS垃圾回收器 (JEP 363)

 

 

不同的垃圾回收器概述

 

  • 爲什麼要有很多收集器,一個不夠嗎?因爲Java的使用場景很多,移端端,服務器等。所以就需要針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的性能。
  • 雖然我們會對各個收集器進行比較,但並非爲了挑選一個最好的收集器出來。沒有一種放之四海皆準、任何場景下都適用的完美收集器存在,更加沒有萬能的收集器。所以我們選擇的只是對具體應用最合適的收集器。

 

如何查看默認的垃圾收集器

-XX:+PrintCommandLineFlags : 查看命令行相關參數(包含使用的垃圾收集器)

使用命令行指令: jinfo -flag 相關垃圾回收器參數 進程ID

 

 

 

3、Serial回收器:串行回收

 

  • Serial收集器是最基本、歷史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的選擇。
  • Serial收集器作爲HotSpot中Client模式下的默認新生代垃圾收集器
  • Serial收集器採用複製算法、串行回收和"Stop-the-World"機制的方式執行內存回收。
  • 除了年輕代之外, Serial收集器還提供用於執行老年代垃圾收集的Serial Old收集器。Serial Old收集器同樣也採用了串行回收和"Stop the World"機制,只不過內存回收算法使用的是標記-壓縮算法。

Serial Old是運行在Client模式下默認的老年代的垃圾回收器

Serial Old在Server模式下主要有兩個用途: ①與新生代的Parallel Scavenge配合使用

②作爲老年代CMS收集器的後備垃圾收集方案

 

 

這個收集器是一個單線程的收集器,但它的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束(stop The world) 。

 

 

  • 優勢:簡單而高效(與其他收集器的單線程比) ,對於限定單個CPU的環境來說, Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

> 運行在Client模式下的虛擬機是個不錯的選擇。

  • 在用戶的桌面應用場景中,可用內存一般不大(幾十MB至一兩百MB) ,可以在較短時間內完成垃圾收集(幾十ms至一百多ms) ,只要不頻繁發生,使用串行回收器是可以接受的。
  • 在HotSpot虛擬機中,使用 -XX:+UseSerialGC 參數可以指定年輕代和老年代都使用串行收集器。

> 等價於新生代用Serial GC,且老年代用Serial old GC

 

 

總結:

這種垃圾收集器大家瞭解,現在已經不用串行的了。而且在限定單核cpu纔可以用。現在都不是單核的了。

對於交互較強的應用而言,這種垃圾收集器是不能接受的。一般在Java web應用程序中是不會採用串行垃圾收集器的。

 

 

 

 

 

4、ParNew回收器:並行回收

 

  • 如果說Serial GC是年輕代中的單線程垃圾收集器,那麼ParNew收集器則是Serial收集器的多線程版本。

> Par是Parallel的縮寫, New:只能處理的是新生代

  • ParNew 收集器除了採用並行回收的方式執行內存回收外,兩款垃圾收集器之間幾乎沒有任何區別。ParNew收集器在年輕代中同樣也是採用複製算法、"Stop-the-World"機制。
  • ParNew 是很多JVM運行在Server模式下新生代的默認垃圾收集器。

 

 

  • 對於新生代,回收次數頻繁,使用並行方式高效。
  • 對於老年代,回收次數少,使用串行方式節省資源。(CPU並行需要切換線程,串行可以省去切換線程的資源)

 

  • 由於ParNew收集器是基於並行回收,那麼是否可以斷定ParNew收集器的回收效率在任何場景下都會比Serial收集器更高效?

ParNew收集器運行在多CPU的環境下,由於可以充分利用多CPU、多核心等物理硬件資源優勢,可以更快速地完成垃圾收集,提升程序的吞吐量。

但是在單個CPU的環境下, ParNew收集器不比Serial收集器更高效。雖然Serial收集器是基於串行回收,但是由於CPU不需要頻繁地做任務切換,因此可以有效避免多線程交互過程中產生的一些額外開銷。

  • 因爲除Serial外, 目前只有ParNew GC能與CMS收集器配合工作

 

 

在程序中,開發人員可以通過選項 "-XX:+UseParNewGC" 手動指定使用ParNew收集器執行內存回收任務。它表示年輕代使用並行收集器,不影向老年代。

-XX:ParallelGCThreads 限制線程數量,默認開啓和CPU數據相同的線程數。

 

 

 

 

 

5、Parallel回收器:吞吐量優先

 

  • HotSpot的年輕代中除了擁有ParNew收集器是基於並行回收的以外,Parallel Scavenge收集器同樣也採用了複製算法、並行回收和"Stop the World" 機制
  • 那麼Parallel 收集器的出現是否多此一舉?

> 和 ParNew 收集器不同, Parallel Scavenge 收集器的目標則是達到一個可控制的吞吐量(Throughput) ,它也被稱爲吞吐量優先的垃圾收集器。

> 自適應調節策略也是 Parallel Scavenge與ParNew一個重要區別。

 

 

  • 高吞吐量則可以高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。因此,常見在服務器環境中使用。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序。
  • Parallel 收集器在JDK1.6時提供了用於執行老年代垃圾收集的Parallel old收集器,用來代替老年代的Serial Old收集器。

Parallel Old收集器採用了標記-壓縮算法,但同樣也是基於並行回收和"Stop-the-World"機制

 

 

 

  • 在程序吞吐量優先的應用場景中, Parallel 收集器和Parallel Old收集器的組合,在Server模式下的內存回收性能很不錯。
  • 在Java8中,默認是此垃圾收集器

 

 

參數配置:

-XX:+UseParallelGC 手動指定年輕代使用Parallel並行收集器執行內存回收任務

-XX:UseParallelOldGC 手動指定老年代都是使用並行回收收集器。

分別適用於新生代和老年代。默認jdk8是開啓的。

上面兩個參數,默認開啓一個,另一個也會被開啓。(互相激活)

-XX:ParallelGCThreads 設置年輕代並行收集器的線程數。一般地,最好與CPU數量相等,以避免過多的線程數影響垃圾收集性能。

在默認情況下,當CPU數量小於8個, ParallelGCThreads 的值等於CPU數量。

當CPU數量大於8個, ParallelGCThreads 的值等於 3+ [5*CPU_Count]/8]。

 

-XX:MaxGCPauseMillis 設置垃圾收集器最大停頓時間(即STW的時間)。單位是毫秒。

爲了儘可能地把停頓時間控制在 MaxGCPauseMills 以內,收集器在工作時會調整Java堆大小或者其他一些參數。

對於用戶來講,停頓時間越短體驗越好。但是在服務器端,我們注重高併發,整體的吞吐量。所以服務器端適合 Parallel,進行控制。

該參數使用需謹慎。

 

-XX:GCTimeRatio 垃圾收集時間佔總時間的比例(= 1 / (N+ 1))。用於衡量吞吐量的大小。

取值範圍(0,100) 。默認值99,也就是垃圾回收時間不超過1%。

與前一個 -XX:MaxGCPauseMillis 參數有一定矛盾性。暫停時間越長, Radio參數就容易超過設定的比例。

 

-XX:+UseAdaptiveSizePolicy 設置Parallel Scavenge 收集器具有自適應調節策略

在這種模式下,年輕代的大小、Eden和Survivor的比例、晉升老年代的對象年齡等參數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。

在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMills) ,讓虛擬機自己完成調優工作。

 

 

 

 

6、CMS回收器:低延遲

 

  • 在JDK 1.5時期, HotSpot 推出了一款在強交互應用中幾乎可認爲有劃時代意義的垃圾收集器: CMS (Concurrent-Mark-Sweep)收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工作。
  • CMS收集器的關注點是儘可能縮短垃圾收集時用戶線程的停頓時間。停頓時間越短(低延遲)就越適合與用戶交互的程序,良好的響應速度能提升用戶體驗。

目前很大一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗CMS收集器就非常符合這類應用的需求。

CMS的垃圾收集算法採用標記-清除算法,並且也會 "Stop-the-world"

 

 

不幸的是, CMS作爲老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。

在G1出現之前, CMS使用還是非常廣泛的。一直到今天,仍然有很多系統使用CMS GC.

 

 

CMS的工作原理

 

CMS整個過程比之前的收集器要複雜,整個過程分爲4個主要階段,即初始標記階段、併發標記階段、重新標記階段和併發清除階段。

  • 初始標記(Initial-Mark)階段:在這個階段中,程序中所有的工作線程都將會因爲"Stop-the-World"機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記出GC Roots能直接關聯到的對象。一旦標記完成之後就會恢復之前被暫停的所有應用線程。由於直接關聯對象比較小,所以這裏的速度非常快。
  • 併發標記(Concurrent-Mark)階段:從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起併發運行。
  • 重新標記(Remark)階段:由於在併發標記階段中,程序的工作線程會和垃圾收集線程同時運行或者交叉運行,因此爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。
  • 併發清除(Concurrent-Sweep)階段:此階段清理刪除掉標記階段判斷的已經死亡的對象,釋放內存空間。由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的

 

 

儘管CMS收集器採用的是併發回收(非獨佔式) ,但是在其初始化標記和再次標記這兩個階段中仍然需要執行"Stop-the-World"機制暫停程序中的工作線程,不過暫停時間並不會太長,因此可以說明目前所有的垃圾收集器都做不到完全不需要"Stop-the-World" ,只是儘可能地縮短暫停時間。

由於最耗費時間的併發標記與併發清除階段都不需要暫停工作,所以整體的回收是低停頓的。

另外, 由於在垃圾收集階段用戶線程沒有中斷,所以在CMS回收過程中,還應該確保應用程序用戶線程有足夠的內存可用。因此, CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,而是當堆內存使用率達到某一閾值時,便開始進行回收,以確保應用程序在CMS工作過程中依然有足夠的空間支持應用程序運行。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次"Concurrent Mode Failure"失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

 

 

CMS收集器的垃圾收集算法採用的是標記-清除算法,這意味着每次執行完內存回收後,由於被執行內存回收的無用對象所佔用的內存空間極有可能是不連續的一些內存塊,不可避免地將會產生一些內存碎片。那麼CMS在爲新對象分配內存空間時,將無法使用指針碰撞(Bump the Pointer)技術,而只能夠選擇空閒列表(Free List)執行內存分配。

 

有人會覺得既然Mark Sweep會造成內存碎片,那麼爲什麼不把算法換成Mark Compact呢?

答案其實很簡答,因爲當併發清除的時候,用Compact整理內存的話,原來的用戶線程使用的內存還怎麼用呢?要保證用戶線程能繼續執行,前提的它運行的資源不受影響嘛。Mark Compact更適合"Stop the World"這種場景下使用

 

 

CMS的優點:

併發收集

低延遲

 

CMS的弊端:

1)會產生內存碎片,導致併發清除後,用戶線程可用的空間不足。在無法分配大對象的情況下,不得不提前觸發Full GC.

2) CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致用戶停頓,但是會因爲佔用了一部分線程而導致應用程序變慢,總吞吐量會降低。

3) CMS收集器無法處理浮動垃圾。可能出現"Concurrent Mode Failure"失敗而導致另一次Full GC的產生。在併發標記階段由於程序的工作線程和垃圾收集線程是同時運行或者交叉運行的,那麼在併發標記階段如果產生新的垃圾對象, CMS將無法對這些垃圾對象進行標記,最終會導致這些新產生的垃圾對象沒有被及時回收,從而只能在下一次執行GC時釋放這些之前未被回收的內存空間

 

 

CMS收集器可以設置的參數

 

-XX:+UseConcMarkSweepGC 手動指定使用CMS收集器執行內存回收任務。

開啓該參數後會自動將 -XX:+UseParNewGC 打開。即: ParNew (Young區用) +CMS (Old區用) +Serial Old的組合。

-XX:CMSlnitiatingOccupanyFraction 設置堆內存使用率的閾值,一旦達到該閾值,便開始進行回收。

JDK5及以前版本的默認值爲68,即當老年代的空間使用率達到68%時,會執行一次CMS回收。JDK6及以上版本默認值爲92%

如果內存增長緩慢,則可以設置一個稍大的值,大的閾值可以有效降低CMS的觸發頻率,減少老年代回收的次數可以較爲明顯地改善應用程序性能。反之,如果應用程序內存使用率增長很快,則應該降低這個閾值,以避免頻繁觸發老年代串行收集器。因此通過該選項便可以有效降低Full GC的執行次數。

 

-XX :+UseCMSCompactAtFullCollection 用於指定在執行完Full GC後對內存空間進行壓縮整理,以此避免內存碎片的產生。不過由於內存壓縮整理過程無法併發執行,所帶來的問題就是停頓時間變得更長了。

-XX:CMSFullGCsBeforeCompaction 設置在執行多少次Full GC後對內存空間進行壓縮整理。

-XX:ParallelCMSThreads 設置CMS的線程數量。

CMS默認啓動的線程數是(ParallelGCThreads+3) /4,

ParallelGCThreads 是年輕代並行收集器的線程數。當CPU資源比較緊張時,受到CMS收集器線程的影響,應用程序的性能在垃圾回收階段可能會非常糟糕

 

 

小結:

Hotspot有這麼多的垃圾回收器,那麼如果有人問, Serial GC

Parallel GC, Concurrent Mark Sweep GC這三個GC有什麼不同呢?

請記住以下口令:

如果你想要最小化地使用內存和並行開銷,請選Serial GC;

如果你想要最大化應用程序的吞吐量,請選Parallel GC;

如果你想要最小化GC的中斷或停頓時間,請選CMS GC.

 

 

JDK 後續版本中CMS的變化

 

JDK9新特性: CMS被標記爲Deprecate了(JEP291)

如果對JDK 9及以上版本的HotSpot虛擬機使用參數-XX:+UseConcMarkSweepGC 來開啓CMS收集器的話,用戶會收到一個警告信息,提示CMS未來將會被廢棄。

JDK14新特性:刪除CMS垃圾回收器(JEP363)

移除了CMS垃圾收集器,如果在JDK14中使用 -XX:+UseConcMarkSweepGC 的話, JVM不會報錯,只是給出一個warning信息,但是不會exit,JVM會自動回退以默認GC方式啓動JVM

 

package com.atguigu.java;

import java.util.ArrayList;

/**
 *  -XX:+PrintCommandLineFlags
 *
 *  -XX:+UseSerialGC:表明新生代使用Serial GC ,同時老年代使用Serial Old GC
 *
 *  -XX:+UseParNewGC:標明新生代使用ParNew GC
 *
 *  -XX:+UseParallelGC:表明新生代使用Parallel GC
 *  -XX:+UseParallelOldGC : 表明老年代使用 Parallel Old GC
 *  說明:二者可以相互激活
 *
 *  -XX:+UseConcMarkSweepGC:表明老年代使用CMS GC。同時,年輕代會觸發對ParNew 的使用
 * @author shkstart  [email protected]
 * @create 2020  0:10
 */
public class GCUseTest {
    public static void main(String[] args) {
        ArrayList<byte[]> list = new ArrayList<>();

        while(true){
            byte[] arr = new byte[100];
            list.add(arr);
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

 

 

7、G1回收器:區域化分代式

 

既然我們已經有了前面幾個強大的GC,爲什麼還要發佈Garbage First (G1)GC?

原因就在於應用程序所應對的業務越來越龐大、複雜,用戶越來越多,沒有GC就不能保證應用程序正常進行,而經常造成STW的GC又跟不上實際的需求,所以纔會不斷地嘗試對GC進行優化。G1 (Garbage-First)垃圾回收器是在Java7 update 4之後引入的一個新的垃圾回收器,是當今收集器技術發展的最前沿成果之一。

與此同時,爲了適應現在不斷擴大的內存和不斷增加的處理器數量,進一步降低暫停時間(pause time) ,同時兼顧良好的吞吐量。

官方給G1設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才擔當起“全功能收集器”的重任與期望。

 

 

爲什麼名字叫做Garbage First (G1)呢?

  • 因爲G1是一個並行回收器,它把堆內存分割爲很多不相關的區域(Region) (物理上不連續的)。使用不同的Region來表示Eden、倖存者0區,倖存者1區,老年代等。
  • G1 GC有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值) ,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region.
  • 由於這種方式的側重點在於回收垃圾最大量的區間(Region) ,所以我們給G1一個名字:垃圾優先(Garbage First)。

 

 

G1 (Garbage-First)是一款面向服務端應用的垃圾收集器,主要針對配備多核CPU及大容量內存的機器,以極高概率滿足GC停頓時間的同時,還兼具高吞吐量的性能特徵。

在JDK1.7版本正式啓用,移除了Experimental的標識,是JDK 9以後的默認垃圾回收器,取代了CMS回收器以及Parallel + Parallel Old組合被Oracle官方稱爲“全功能的垃圾收集器”

與此同時, CMS已經在JDK 9中被標記爲廢棄(deprecated) 。在jdk8中還不是默認的垃圾回收器,需要使用-XX:+UseG1GC來啓用

 

 

與其他GC收集器相比, G1使用了全新的分區算法,其特點如下所示:

並行與併發

並行性: G1在回收期間,可以有多個GC線程同時工作,有效利用多核計算能力。此時用戶 程STW;

併發性: G1擁有與應用程序交替執行的能力,部分工作可以和應用程序同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程序的情況;

分代收集

從分代上看, G1依然屬於分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和Survivor區,但從堆的結構上看,它不要求整個Eden區、年輕代或者老年代都是連續的,也不再堅持固定大小和固定數量。

將堆空間分爲若干個區域(Region) ,這些區域中包含了邏輯上的年輕代和老年代。

和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;

 

 

空間整合

CMS: “標記-清除”算法、內存碎片、若干次GC後進行一次碎片整理

G1將內存劃分爲一個個的region,內存的回收是以region作爲基本單位的。Region之間是複製算法,但整體上實際可看作是標記-壓縮(Mark-Compact)算法,兩種算法都可以避免內存碎片。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次GC,尤其是當Java堆非常大的時候, G1的優勢更加明顯。

 

 

可預測的停頓時間模型(即:軟實時soft real-time)

這是G1相對於CMS的另一大優勢, G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

由於分區的原因, G1可以只選取部分區域進行內存回收,這樣縮小了回收的範圍,因此對於全局停頓情況的發生也能得到較好的控制。

G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值) ,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

相比於CMS GC, G1未必能做到CMS在最好情況下的延時停頓,但是最差情況要好很多。

 

 

 

G1回收器的缺點

 

相較於CMS, G1還不具備全方位、壓倒性優勢。比如在用戶程序運行過程中,G1無論是爲了垃圾收集產生的內存佔用(Footprint)還是程序運行時的額外執行負載(Overload)都要比CMS要高。

從經驗上來說,在小內存應用上CMS的表現大概率會優於G1,而G1在大內存應用上則發揮其優勢。平衡點在6-8GB之間。

 

 

G1回收器的參數設置

 

  • -XX:+UseG1GC 手動指定使用G1收集器執行內存回收任務
  • -XX:G1HeapRegionSize 設置每個Region的大小。值是2的冪,範圍是1MB到32MB之間, 目標是根據最小的Java堆大小劃分出約2048個區域。默認是堆內存的1/2000,
  • -XX:MaxGCPauseMillis 設置期望達到的最大GC停頓時間指標(JVM會盡力實現,但不保證達到)。默認值是200ms
  • -XX:ParallelGCThread 設置STW時GC線程數的值。最多設置爲8
  • -XX:ConcGCThreads 設置併發標記的線程數。將n設置爲並行垃圾回收線程數(ParallelGCThreads)的1/4左右。
  • -XX:InitiatinqHeapOccupancyPercent 設置觸發併發GC週期的Java堆佔用率閾值。超過此值,就觸發GC。默認值是45

 

 

G1回收器的常見操作步驟

 

G1的設計原則就是簡化JVM性能調優,開發人員只需要簡單的三步即可完成調優:

 

第一步:開啓G1垃圾收集器

第二步:設置堆的最大內存

第三步:設置最大的停頓時間

 

G1中提供了三種垃圾回收模式: YoungGC、Mixed GC和Full GC,在不同的條件下被觸發。

 

 

G1回收器的適用場景

 

  • 面向服務端應用,針對具有大內存、多處理器的機器。(在普通大小的堆裏表現並不驚喜)
  • 最主要的應用是需要低GC延遲,並具有大堆的應用程序提供解決方案;
  • 如:在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒; (G1通過每次只清理一部分而不是全部的Region的增量式清理來保證每次GC停頓時間不會過長)
  • 用來替換掉JDK1.5中的CMS收集器;

①超過50%的Java堆被活動數據佔用;

②對象分配頻率或年代提升頻率變化很大

③GC停頓時間過長(長於0.5至1秒)

 

  • HotSpot 垃圾收集器裏,除了G1以外,其他的垃圾收集器使用內置的JVM線程執行GC的多線程操作,而G1 GC可以採用應用線程承擔後臺運行的GC工作,即當JVM的GC線程處理速度慢時,系統會調用應用程序線程幫助加速垃圾回收過程。

 

 

分區Region :化整爲零

 

使用G1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據堆空間的實際大小而定,整體被控制在1MB到32ME之間,且爲2的N次冪,即1MB, 2MB, 4MB, 8MB, 16MB, 32MB,可以通過

-XX:G1HeapRegionSize 設定。所有的Region大小相同,且在JVM生命週期內不會被改變。

雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region (不需要連續)的集合。通過Region的動態分配方式實現邏輯上的連續。

 

 

 

  • 一個 region有可能屬於Eden, Survivor或者Old/Tenured 內存區域。但是一個region只可能屬於一個角色。圖中的E表示該region屬於Eden內存區域, S表示屬於Survivor內存區域, O表示屬於Old內存區域。圖中空白的表示未使用的內存空間。
  • G1垃圾收集器還增加了一種新的內存區域,叫做 Humongous 內存區域,如圖中的H塊。主要用於存儲大對象,如果超過1.5個region,就放到H。

 

 

設置H的原因:

對於堆中的大對象,默認直接會被分配到老年代,但是如果它是一個短期存在的大對象,就會對垃圾收集器造成負面影響。爲了解決這個問題, G1劃分了一個Humongous區,它用來專門存放大對象。如果一個H區裝不下一個大對象,那麼G1會尋找連續的H區來存儲。爲了能找到連續的H區,有時候不得不啓動Full GC. G1的大多數行爲都把H區作爲老年代的一部分來看待。

 

  • Bump - the - pointer

即:指針碰撞

  • TLAB

 

 

G1回收器垃圾回收過程

G1 GC的垃圾回收過程主要包括如下三個環節:

年輕代GC (Young GC)

老年代併發標記過程(Concurrent Marking)

混合回收(Mixed Gc)

(如果需要,單線程、獨佔式、高強度的Full GC還是繼續存在的。它計對GC的評估失敗提供了一種失敗保護機制,即強力回收。)

 

順時針, young gc -> young gc + concurrent mark-> Mixed GC順序,進行垃圾回收。

 

應用程序分配內存,當年輕代的Eden區用盡時開始年輕代回收過程; G1的年輕代收集階段是一個並行的獨佔式收集器。在年輕代回收期, G1 GC暫停所有應用程序線程,啓動多線程執行年輕代回收。然後從年輕代區間移動存活對象到Survivor區間或者老年區間,也有可能是兩個區間都會涉及

 

當堆內存使用達到一定值(默認45%)時,開始老年代併發標記過程。

 

標記完成馬上開始混合回收過程。對於一個混合回收期, G1 GC從老年區間移動存活對象到空閒區間,這些空閒區間也就成爲了老年代的一部分。和年輕代不同,老年代的G1回收器和其他GC不同, G1的老年代回收器不需要整個老年代被回收,一次只需要掃描/回收一小部分老年代的Region就可以了。同時,這個老年代Region是和年輕代一起被回收的。

舉個例子:一個Web服務器, Java進程最大堆內存爲4G,每分鐘響應1500個請求,每45秒鐘會新分配大約2G的內存。G1會每45秒鐘進行一次年輕代回收,每31個小時整個堆的使用率會達到45%,會開始老年代併發標記過程,標記完成後開始四到五次的混合回收。

 

 

G1回收器垃圾回收過程: Remembered Set

  • 一個對象被不同區域引用的問題
  • 一個Region不可能是孤立的,一個Region中的對象可能被其他任意Region中對象引用,判斷對象存活時,是否需要掃描整個Java堆才能保證準確?
  • 在其他的分代收集器,也存在這樣的問題(而G1更突出)
  • 回收新生代也不得不同時掃描老年代?
  • 這樣的話會降低Minor GC的效率;

 

解決方法:

  • 無論G1還是其他分代收集器, JVM都是使用Remembered Set來避免全局掃描:
  • 每個Region都有一個對應的Remembered Set;
  • 每次Reference類型數據寫操作時,都會產生一個Write Barrier暫時中斷操作;
  • 然後檢查將要寫入的引用指向的對象是否和該Reference類型數據在不同的Region (其他收集器:檢查老年代對象是否引用了新生代對象)
  • 如果不同,通過CardTable把相關引用信息記錄到引用指向對象的所在Region對應的Remembered Set中;
  • 當進行垃圾收集時,在GC根節點的枚舉範圍加入Remembered Set;就可以保證不進行全局掃描,也不會有遺漏。

 

 

 

G1回收過程一:年輕代GC

JVM啓動時, G1先準備好Eden區,程序在運行過程中不斷創建對象到Eden區,當Eden空間耗盡時, G1會啓動一次年輕代垃圾回收過程。

年輕代垃圾回收只會回收Eden區和Survivor區。

YGC時,首先G1停止應用程序的執行(Stop-The-World) , G1創建回收集

(Collection Set) ,回收集是指需要被回收的內存分段的集合,年輕代回收過程的回收集包含年輕代Eden區和Survivor區所有的內存分段。

 

然後開始如下回收過程:

第一階段,掃描根。

根是指static變量指向的對象,正在執行的方法調用鏈條上的局部變量等。根引用連同RSet記錄的外部引用作爲掃描存活對象的入口。

第二階段,更新RSet

處理dirty card queue(見備註)中的card,更新RSet。此階段完成後, RSet可以準確的反映老年代對所在的內存分段中對象的引用。

第三階段,處理RSet

識別被老年代對象指向的Eden中的對象,這些被指向的Eden中的對象被認爲是存活的對象。

第四階段,複製對象。

此階段,對象樹被遍歷, Eden區內存段中存活的對象會被複制到Survivor區中空的內存分段,Survivor區內存段中存活的對象如果年齡未達閾值,年齡會加1,達到閥值會被會被複制到Old區中空的內存分段。如果Survivor空間不夠, Eden空間的部分數據會直接晉升到老年代空間。

第五階段,處理引用。

處理Soft, Weak, Phantom, Final, JNI Weak等引用。最終Eden空間的數據爲空, GC停止工作,而目標內存中的對象都是連續存儲的,沒有碎片,所以複製過程可以達到內存整理的效果,減少碎片。

 

 

G1回收過程二:併發標記過程

 

1,初始標記階段:標記從根節點直接可達的對象。這個階段是STW的,並且會觸發一次年輕代GC.

2,根區域掃描(Root Region Scanning) : G1 GC掃描Survivor區直接可達的老年代區域對象,並標記被引用的對象。這一過程必須在young GC之前完成。

3,併發標記(Concurrent Marking):在整個堆中進行併發標記(和應用程序併發執行),此過程可能被young GC中斷。在併發標記階段,若發現區域對象中的所有對象都是垃圾,那這個區域會被立即回收。同時,併發標記過程中,會計算每個區域的對象活性(區域中存活對象的比例)。

4,再次標記(Remark): 由於應用程序持續進行,需要修正上一次的標記結果。是STW的。G1中採用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。

5,獨佔清理(cleanup, STw):計算各個區域的存活對象和GC回收比例,並進行排序,識別可以混合回收的區域。爲下階段做鋪墊。是STW的。

> 這個階段並不會實際上去做垃圾的收集

6,併發清理階段:識別並清理完全空閒的區域。

 

 

G1回收過程三:混合回收

當越來越多的對象晉升到老年代old region時,爲了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即Mixed GC,該算法並不是一個Old GC,除了回收整個Young Region,還會回收一部分的Old Region。這裏需要注意:是一部分老年代,而不是全部老年代。可以選擇哪些Old Region進行收集,從而可以對垃圾回收的耗時時間進行控制。也要注意的是Mixed GC並不是Full GC.

 

  • 併發標記結束以後,老年代中百分百爲垃圾的內存分段被回收了,部分爲垃圾的內存分段被計算了出來。默認情況下,這些老年代的內存分段會分8次(可以通過 -XX:G1MixedGCCountTarget設置)被回收。
  • 混合回收的回收集(Collection Set)包括八分之一的老年代內存分段, Eden區內存分段, Survivor區內存分段。混合回收的算法和年輕代回收的算法完全一樣,只是回收集多了老年代的內存分段。具體過程請參考上面的年輕代回收過程。
  • 由於老年代中的內存分段默認分8次回收, G1會優先回收垃圾多的內存分段。垃圾佔內存分段比例越高的,越會被先回收。並且有一個閾值會決定內存分段是否被回收,

-XX:G1MixedGCLiveThresholdPercent,默認爲65%,意思是垃圾佔內存分段比例要達到65%纔會被回收。如果垃圾佔比太低,意味着存活的對象佔比高,在複製的時候會花費更多的時間。

  • 混合回收並不一定要進行8次。有一個閾值-XX:G1HeapWastePercent,默認值爲10%,意思是允許整個堆內存中有10%的空間被浪費,意味着如果發現可以回收的垃圾佔堆內存的比例低於10%,則不再進行混合回收。因爲GC會花費很多的時間但是回收到的內存卻很少。

 

 

G1回收可選的過程四: Full GC

 

G1的初衷就是要避免Full GC的出現。但是如果上述方式不能正常工作, G1會停止應用程序的執行(Stop-The-World) ,使用單線程的內存回收算法進行垃圾回收,性能會非常差,應用程序停頓時間會很長。

要避免Full GC的發生,一旦發生需要進行調整。什麼時候會發生Full GC,呢?比如堆內存太小,當G1在複製存活對象的時候沒有空的內存分段可用,則會回退到full gc,這種情況可以通過增大內存解決。

導致G1 Full GC的原因可能有兩個:

1. Evacuation的時候沒有足夠的to-space來存放晉升的對象;

2·併發處理過程完成之前空間耗盡。

 

 

G1回收過程:補充

從Oracle官方透露出來的信息可獲知,回收階段(Evacuation)其實本也有想過設計成與用戶程序一起併發執行,但這件事情做起來比較複雜,考慮到G1只是回收一部分Region,停頓時間是用戶可控制的,所以並不迫切去實現,而選擇把這個特性放到了G1之後出現的低延遲垃圾收集器(即ZGc)中。另外,還考慮到G1不是僅僅面向低延遲,停頓用戶線程能夠最大幅度提高垃圾收集效率,爲了保證吞吐量所以才選擇了完全暫停用戶線程的實現方案。

 

 

G1回收器優化建議

年輕代大小

> 避免使用 -Xmn 或 -XX:NewRatio 等相關選項顯式設置年輕代大小

> 固定年輕代的大小會覆蓋暫停時間目標

暫停時間目標不要太過嚴苛

> G1 GC的吞吐量目標是90%的應用程序時間和10%的垃圾回收時間

> 評估G1 GC的吞吐量時,暫停時間目標不要太嚴苛。目標太過嚴苛表示你願意承受更多的垃圾回收開銷,而這些會直接影響到吞吐量。

 

 

 

 

 

8、垃圾回收器總結

 

7種經典垃圾回收器總結

 

截止JDK 1.8,一共有7款不同的垃圾收集器。每一款不同的垃圾收集器都有不同的特點,在具體使用的時候,需要根據具體的情況選用不同的垃圾收集器。

 

 

 

怎麼選擇垃圾回收器?

 

  • Java垃圾收集器的配置對於JVM優化來說是一個很重要的選擇,選擇合適的垃圾收集器可以讓JVM的性能有一個很大的提升。
  • 怎麼選擇垃圾收集器?

1,優先調整堆的大小讓JVM自適應完成。

2,如果內存小於100M,使用串行收集器

3·如果是單核、單機程序,並且沒有停頓時間的要求,串行收集器

4,如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇並行或者JVM自己選擇

5·如果是多CPU、追求低停頓時間,需快速響應(比如延遲不能超過1秒,如互聯網應用) ,使用併發收集器

官方推薦G1,性能高。現在互聯網的項目,基本都是使用G1。

 

 

最後需要明確一個觀點:

1,沒有最好的收集器,更沒有萬能的收集;

2,調優永遠是針對特定場景、特定需求,不存在一勞永逸的收集器

 

 

 

面試

  • 對於垃圾收集,面試官可以循序漸進從理論、實踐各種角度深入,也未必是要求面試者什麼都懂。但如果你懂得原理,一定會成爲面試中的加分項。這裏較通用、基礎性的部分如下:

> 垃圾收集的算法有哪些?如何判斷一個對象是否可以回收?

> 垃圾收集器工作的基本流程。

  • 另外,大家需要多關注垃圾回收器這一章的各種常用的參數。

 

 

9、GC日誌分析

 

 

通過閱讀GC日誌,我們可以瞭解Java虛擬機內存分配與回收策略。

內存分配與垃圾回收的參數列表

-XX:+PrintGC 輸出GC日誌。類似: -verbose:gc

-XX:+PrintGCDetails 輸出GC的詳細日誌

-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)

-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如2013-05-04121:53:59.234+0800)

-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的信息

-Xloggc: ../logs/gc.log 日誌文件的輸出路徑

 

 

日誌補充說明:

  • "[GC"和"[Full GC"說明了這次垃圾收集的停頓類型,如果有"Full"則說明GC發生了"StopThe world"
  • 使用Serial收集器在新生代的名字是Default New Generation,因此顯示的是"[DefNew"
  • 使用ParNew收集器在新生代的名字會變成" [ParNew",意思是"Parallel New Generation"
  • 使用Parallel Scavenge收集器在新生代的名字是"[PSYoungGen"
  • 老年代的收集和新生代道理一樣,名字也是收集器決定的
  • 使用G1收集器的話,會顯示爲"garbage-first heap"

 

  • Allocation Failure

表明本次引起GC的原因是因爲在年輕代中沒有足夠的空間能夠存儲新的數據了。

  • [PSYoungGen: 5986K->696K (8704K) 5986K->704K (9216K)

中括號內: GC回收前年輕代大小,回收後大小, (年輕代總大小)

括號外: GC回收前年輕代和老年代大小,回收後大小, (年輕代和老年代總大小)

  • user代表用戶態回收耗時, sys內核態回收耗時, rea實際耗時。由於多核的原因,時間總和可能會超過real時間

 

Minor GC 日誌

 

Full GC 日誌

 

可以用一些工具去分析這些gc日誌。

常用的日誌分析工具有: GCViewer, GCEasy, GCHisto, GCLogViewer,Hpjmeter,garbagecat等。

 

 

 

 

 

10、垃圾回收器的新發展

 

GC仍然處於飛速發展之中, 目前的默認選項G1 GC在不斷的進行改進,很多我們原來認爲的缺點,例如串行的Full GC, Card Table掃描的低效等,都已經被大幅改進,例如, JDK 10以後, Full GC已經是並行運行,在很多場景下,其表現還略優於Parallel GC的並行Full GC實現。

即使是Serial GC,雖然比較古老,但是簡單的設計和實現未必就是過時的,它本身的開銷,不管是GC相關數據結構的開銷,還是線程的開銷,都是非常小的,所以隨着雲計算的興起,在Serverless等新的應用場景下, Serial GC找到了新的舞臺。

比較不幸的是CMS GC,因爲其算法的理論缺陷等原因,雖然現在還有非常大的用戶羣體,但在JDK9中已經被標記爲廢棄,並在JDK14版本中移除。

 

 

令人震驚、革命性的ZGC

https://docs.oracle.com/en/java/javase/12/gctuning/

 

 

ZGC與Shenandoah目標高度相似,在儘可能對吞吐量影響不大的前提下,實現在任意堆內存大小下都可以把垃圾收集的停頓時間限制在十毫秒以內的低延遲。

《深入理解Java虛擬機》一書中這樣定義ZGC: ZGC收集器是一款基於Region內存佈局的, (暫時)不設分代的,使用了讀屏障、染色指針和內存多重映射等技術來實現可併發的標記-壓縮算法的,以低延遲爲首要目標的一款垃圾收集器。

ZGC的工作過程可以分爲4個階段:併發標記-併發預備重分配-併發重分配-併發重映射等。

ZGC幾乎在所有地方併發執行的,除了初始標記的是STW的。所以停頓時間幾乎就耗費在初始標記上,這部分的實際時間是非常少的。

 

 

 

 

 

 

 

 

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