學習JVM-GC收集器


1. 前言

  在上一篇文章中,介紹了JVM中垃圾回收的原理和算法。介紹了通過引用計數和對象可達性分析的算法來篩選出已經沒有使用的對象,然后介紹了垃圾收集器中使用的三種收集算法:標記-清除、標記-整理、標記-復制算法。

  介紹完原理,在這篇文章中,我們將介紹當前JVM中已經實現的垃圾收集器,以及與收集器主題相關的一些內容。

  首先,我們將在上一篇文章中提到分代收集機制的基礎上,介紹下現代商業JVM中普遍采用的分代回收策略。然后,按照內存分代划分的維度介紹下當前JVM中實現的收集器。最后,學習分析不同收集器的GC日志,然后結合日志分析,學習下不同情況下的對象分配策略。

2. 分代收集策略

  我們知道,當對象被創建的時候,就會給對象分配一塊內存空間,而一旦對象的生命周期結束,我們就需要回收這塊內存空間。但是,在一個應用程序中,不同的對象存在的時間,或者說每個對象的生命周期都是不同的。

  有些對象生命周期很短,比如Web應用程序中的request對象,它的生命周期和請求是對應的,當請求完成以后,該request對象就結束了它的職責,需要被收集器回收。有些對象的生命周期很長,比如一些全局的對象,可能會伴隨整個應用程序的生命周期而存在。

  在上圖中,橫軸表示對象的生命周期長短,豎軸表示對應生命周期下的對象數量。觀察藍色的區域,我們可以看到大部分的對象的生命周期都很短,而生命周期長的對象,它們的數量占據了小部分。

  考慮到不同生命周期的對象的分布情況,為了合理的處理不同生命周期的對象回收問題。現代JVM的對不同生命周期的對象進行分類,對堆內存區域進行邏輯划分。按照對象的存活時間長短,將內存分為:年輕代、老年代和永久代(在Java8中去掉了永久代,以元數據空間代替)。這里我們主要關注年輕代和老年代的GC。

  JVM提供了兩個參數來控制JVM堆的大小:-XX:InitialHeapSize(-Xms)-XX:MaxHeapSize(-Xmx)。JVM會根據應用程序使用內存的情況,動態擴展堆內存的大小,上圖中的Virtual表示的區域,表示的就是可以擴展的內存空間。

  比如,我們可以將JVM的堆內存設置為256M,最大512M的大小,那么可以這么設置:-Xms256m -Xmx512m。如果將Xms的值和Xmx的值設置為相同,那么JVM將不能動態擴展堆內存,它的初始堆內存和最大堆內存是相同的。

2.1 年輕代

  在年輕代中,又將內存細分為Eden區和2個Survivor區,正常情況下,對象都是在Eden區被分配的。由於在年輕代,GC算法采用的是“標記-復制”算法,所以划分出了兩個Survivor區,用於在執行復制算法的時候交替存放存活的對象。

  在JVM運行的時候,在年輕代,只有Eden區和其中的一個Survivor區會被使用,而另外一個Surviro區是閑置的。當在年輕代進行GC的時候,會將這次GC以后存活的對象移動到其中閑置的Survivor區中,然后清空Eden區和之前的Survivor區。這樣,就可以保證每次都有一快空閑的內存用於復制。

  JVM參數NewSizeMaxNewSize分別可以控制年輕代的初始大小和最大的大小。通過設置這兩個參數,可以手動控制JVM中年輕代的大小,比如-XX:NewSize=100m將年輕代的大小初始化為100m。除了通過固定值來控制年輕代的大小,還可以通過參數NewRatio來按比例控制年輕代的大小,NewRatio的值表示年輕代和老年代的比值,比如:-XX:NewRatio=6 就表示,年輕代:老年代 = 1:6,所以年輕代占據了堆內存的1/7,而老年代則占據了6/7。

  對於年輕代中的Eden區和Survivor區的大小分配,JVM提供了SurvivorRatio這個參數來控制兩塊區域的大小。和NewRatio一樣,這個值也是用於控制Eden區和兩個Survivor區的大小比例的,比如:-XX:SurvivorRatio = 8,那么表示Eden : 一個Survivor = 8 : 1,那么Eden區就占據了年輕代中的8 / 10,而兩個Survivor區分別占據了 1 / 10。

  我們一般把在年輕代中進行的GC稱為Minor GC

2.2 老年代

  當對象在年輕代中經歷了多次Minor GC以后仍舊存活,那么當達到一定的年齡(經歷過一次Minor GC,年齡加1)以后,仍舊存活的對象就會被移動到老年代中。在老年代中的對象,一般是那些存活時間相對比較長的對象。正常情況下,在老年代的GC不會像年輕代那么頻繁,老年代的GC收集器,一般采用"標記-清除"算法或"標記-整理"算法來回收垃圾對象。

  老年代中的對象,除了通過年輕代提升上來的長生命周期的對象以外,在一些特殊的情況下,也會在老年代中直接分配對象。具體情況,在下面的對象分配策略一節,會具體講述。

  老年代的GC我們一般稱為Major GC

3. GC收集器

  前面,我們介紹了JVM中對堆內存進行了分代的划分。目的,就是為了可以按照不同的對象特點,合理的利用不同的垃圾收集算法來處理垃圾對象。接下來,我們來看下針對不同的內存區域和使用場景,JVM中已經實現的那些GC收集器,了解下不同的收集器的特性和適用場景以及它們的優缺點。

3.1 收集器概覽

  Oracle Hotspot JVM中實現了多種垃圾收集器,針對不同的年齡代內存中的對象的生存周期和應用程序的特點,實現了多款垃圾收集器。

  單線程GC收集器包括Serial和SerialOld這兩款收集器,分別用於年輕代和老年代的垃圾收集工作。后來,隨着CPU多核的普及,為了更好了利用多核的優勢,開發了ParNew收集器,這款收集器是Serial收集器的多線程版本。

  多線程收集器還包括Parallel Scavenge和ParallelOld收集器,這兩款也分別用於年輕代和老年代的垃圾收集工作,不同的是,它們是兩款可以利用多核優勢的多線程收集器。

  相對來說更加復雜的還有CMS收集器。這款收集器,在運行的時候會分多個階段進行垃圾收集,而且在一些階段是可以和應用線程並行運行的,提高了這款收集器的收集效率。

  其中最先進的收集器,要數G1這款收集器了。這款收集器是當前最新發布的收集器,是一款面向服務端垃圾收集器。

  上面簡單介紹了多款不同的垃圾收集器,雖然它們的特性不同,但是有些GC收集器可以組合使用來應對不同的應用的業務場景。下圖給出了不同收集器以及它們之間是否兼容,互相兼容的收集器可以組合使用。

  接下來,我們來分別介紹下上面提到的那些GC收集器以及它們各自的特點。

3.2 年輕代收集器

  年輕代收集器包括Serial收集器、ParNew收集器以及Parallel Scavenge收集器。

Serial收集器

  Serial收集器是一款年輕代的垃圾收集器,使用標記-復制垃圾收集算法。它是一款發展歷史最悠久的垃圾收集器。Serial收集器只能使用一條線程進行垃圾收集工作,並且在進行垃圾收集的時候,所有的工作線程都需要停止工作,等待垃圾收集線程完成以后,其他線程才可以繼續工作。工作過程可以簡單的用下圖來表示:  

   從圖中可以看到,Serial收集器工作的時候,其他用戶線程都停止下來,等到GC過程結束以后,它們才繼續執行。而且處理GC過程的只有一條線程在執行。由於Serial收集器的這種工作機制,所以在進行垃圾收集過程中,會出現STW(Stop The World)的情況,應用程序會出現停頓的狀況。如果垃圾收集的時間很長,那么停頓時間也會很長,這樣會導致系統響應變的遲鈍,影響系統的時候。

  雖然這款年邁的垃圾收集器只能使用單核CPU,但是正是由於它不能利用多核,在一些場景下,減少了很多線程的上下文切換的開銷,可以在進行垃圾收集過程中專心處理GC過程,而不會被打斷,所以如果GC過程很短暫,那么這款收集器還是非常簡單高效的。

  由於Serial收集器只能使用單核CPU,在現代處理器基本都是多核多線程的情況下,為了充分利用多核的優勢,出現了多線程版本的垃圾收集器,比如下面將要說到的ParNew收集器。

ParNew收集器

  ParNew垃圾收集器是Serial收集器的多線程版本,使用標記-復制垃圾收集算法。為了利用CPU多核多線程的優勢,ParNew收集器可以運行多個收集線程來進行垃圾收集工作。這樣可以提高垃圾收集過程的效率。

  和上面的Serial收集器比較,可以明顯看到,在垃圾收集過程中,GC線程是多線程執行的,而在Serial收集器中,只有一個GC線程在處理垃圾收集過程。ParNew收集器在很多時候都是作為服務端的年輕代收集器的選擇,除了它具有比Serial收集器更好的性能外,還有一個原因是,多線程版本的年輕代收集器中,只有它可以和CMS這款優秀的老年代收集器一起搭配搭配使用。

  作為一款多線程收集器,當它運行在單CPU的機器上的時候,由於不能利用多核的優勢,在線程收集過程中可能會出現頻繁上下文切換,導致額外的開銷,所以在單CPU的機器上,ParNew收集器的性能不一定好於Serial這款單線程收集器。如果機器是多CPU的,那么ParNew還是可以很好的提高GC收集的效率的。

  ParNew收集器默認開啟的垃圾收集線程數是和當前機器的CPU數量相同的,為了控制GC收集線程的數量,可以通過參數-XX:ParallelGCThreads來控制垃圾收集線程的數量。

Parallel Scavenge收集器

  Parallel Scavenge收集器是是一款年輕代的收集器,它使用標記-復制垃圾收集算法。和ParNew一樣,它也會一款多線程的垃圾收集器,但是它又和ParNew有很大的不同點。

  Parallel Scavenge收集器和其他收集器的關注點不同。其他收集器,比如ParNew和CMS這些收集器,它們主要關注的是如何縮短垃圾收集的時間。而Parallel Scavenge收集器關注的是如何控制系統運行的吞吐量。這里說的吞吐量,指的是CPU用於運行應用程序的時間和CPU總時間的占比,吞吐量 = 代碼運行時間 / (代碼運行時間 + 垃圾收集時間)。如果虛擬機運行的總的CPU時間是100分鍾,而用於執行垃圾收集的時間為1分鍾,那么吞吐量就是99%。

  直觀上,好像以縮短垃圾收集的停頓時間為目的和以控制吞吐量為目的差不多,但是適用的場景卻不同。對於那些桌面應用程序,為了得到良好的用戶體驗,在交互過程中,需要得到快速的響應,所以系統的停頓時間要盡可能的快以避免影響到系統的響應速度,只要保證每次停頓的時間很短暫,假設每次停頓時間為10ms,那么即使發生很多次的垃圾收集過程,假設1000次,也不會影響到系統的響應速度,不會影響到用戶的體驗。對於一些后台計算任務,它不需要和用戶進行交互,所以短暫的停頓時間對它而言並不需要,對於計算任務而言,更好的利用CPU時間,提高計算效率才是需要的,所以假設每次停頓時間相對很長,有100ms,而由於花費了很長的時間進行垃圾收集,那么垃圾收集的次數就會降下來,假設只有5次,那么顯然,使用以吞吐量為目的的垃圾收集器,可以更加有效的利用CPU來完成計算任務。所以,在用戶界面程序中,使用低延遲的垃圾收集器會有很好的效果,而對於后台計算任務的系統,高吞吐量的收集器才是首選。

  Parallel Scavenge收集器提供了兩個參數用於控制吞吐量。-XX:MaxGCPauseMillis用於控制最大垃圾收集停頓時間,-XX:GCTimeRatio用於直接控制吞吐量的大小。MaxGCPauseMillis參數的值允許是一個大於0的整數,表示毫秒數,收集器會盡可能的保證每次垃圾收集耗費的時間不超過這個設定值。但是如果這個這個值設定的過小,那么Parallel Scavenge收集器為了保證每次垃圾收集的時間不超過這個限定值,會導致垃圾收集的次數增加和增加年輕代的空間大小,垃圾收集的吞吐量也會隨之下降。GCTimeRatio這個參數的值應該是一個0-100之間的整數,表示應用程序運行時間和垃圾收集時間的比值。如果把值設置為19,即系統運行時間 : GC收集時間 = 19 : 1,那么GC收集時間就占用了總時間的5%(1 / (19 + 1) = 5%),該參數的默認值為99,即最大允許1%(1 / (1 + 99) = 1%)的垃圾收集時間。

  Parallel Scavenge收集器還有一個參數:-XX:UseAdaptiveSizePolicy。這是一個開關參數,當開啟這個參數以后,就不需要手動指定新生代的內存大小(-Xmn)、Eden區和Survivor區的比值(-XX:SurvivorRatio)以及晉升到老年代的對象的大小(-XX:PretenureSizeThreshold)等參數了,虛擬機會根據當前系統的運行情況動態調整合適的設置值來達到合適的停頓時間和合適的吞吐量,這種方式稱為GC自適應調節策略。

  Parallel Scavenge收集器也是一款多線程收集器,但是由於目的是為了控制系統的吞吐量,所以這款收集器也被稱為吞吐量優先收集器。

3.3 老年代收集器

  老年代收集包括:Serial Old收集器、Parallel Old收集器以及CMS收集器。

Serial Old收集器

  Serial Old收集器是Serial收集器的老年代版本,它也是一款使用"標記-整理"算法的單線程的垃圾收集器。這款收集器主要用於客戶端應用程序中作為老年代的垃圾收集器,也可以作為服務端應用程序的垃圾收集器,當它用於服務端應用系統中的時候,主要是在JDK1.5版本之前和Parallel Scavenge年輕代收集器配合使用,或者作為CMS收集器的后備收集器。

Parallel Old收集器

  Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用"標記-整理"算法。這個收集器是在JDK1.6版本中出現的,所以在JDK1.6之前,新生代的Parallel Scavenge只能和Serial Old這款單線程的老年代收集器配合使用。Parallel Old垃圾收集器和Parallel Scavenge收集器一樣,也是一款關注吞吐量的垃圾收集器,和Parallel Scavenge收集器一起配合,可以實現對Java堆內存的吞吐量優先的垃圾收集策略。

  Parallel Old垃圾收集器的工作原理和Parallel Scavenge收集器類似。

  

CMS收集器

  CMS收集器是目前老年代收集器中比較優秀的垃圾收集器。CMS是Concurrent Mark Sweep,從名字可以看出,這是一款使用"標記-清除"算法的並發收集器。CMS
垃圾收集器是一款以獲取最短停頓時間為目標的收集器。由於現代互聯網中的應用,比較重視服務的響應速度和系統的停頓時間,所以CMS收集器非常適合在這種場景下使用。

  CMS收集器的運行過程相對上面提到的幾款收集器要復雜一些。  

  從圖中可以看出,CMS收集器的工作過程可以分為4個階段:

  • 初始標記(CMS initial mark)階段
  • 並發標記(CMS concurrent mark)階段
  • 重新標記(CMS remark)階段
  • 並發清除(CMS concurrent sweep)階段

  從圖中可以看出,在這4個階段中,初始標記和重新標記這兩個階段都是只有GC線程在運行,用戶線程會被停止,所以這兩個階段會發送STW(Stop The World)。初始標記階段的工作是標記GC Roots可以直接關聯到的對象,速度很快。並發標記階段,會從GC Roots 出發,標記處所有可達的對象,這個過程可能會花費相對比較長的時間,但是由於在這個階段,GC線程和用戶線程是可以一起運行的,所以即使標記過程比較耗時,也不會影響到系統的運行。重新標記階段,是對並發標記期間因用戶程序運行而導致標記變動的那部分記錄進行修正,重新標記階段耗時一般比初始標記稍長,但是遠小於並發標記階段。最終,會進行並發清理階段,和並發標記階段類似,並發清理階段不會停止系統的運行,所以即使相對耗時,也不會對系統運行產生大的影響。

  由於並發標記和並發清理階段是和應用系統一起執行的,而初始標記和重新標記相對來說耗時很短,所以可以認為CMS收集器在運行過程中,是和應用程序是並發執行的。由於CMS收集器是一款並發收集和低停頓的垃圾收集器,所以CMS收集器也被稱為並發低停頓收集器。

  雖然CMS收集器可以是實現低延遲並發收集,但是也存在一些不足。

  首先,CMS收集器對CPU資源非常敏感。對於並發實現的收集器而言,雖然可以利用多核優勢提高垃圾收集的效率,但是由於收集器在運行過程中會占用一部分的線程,這些線程會占用CPU資源,所以會影響到應用系統的運行,會導致系統總的吞吐量降低。CMS默認開始的回收線程數是(Ncpu + 3) / 4,其中Ncpu是機器的CPU數。所以,當機器的CPU數量為4個以上的時候,垃圾回收線程將占用不少於%25的CPU資源,並且隨着CPU數量的增加,垃圾回收線程占用的CPU資源會減少。但是,當CPU資源少於4個的時候,垃圾回收線程占用的CPU資源的比例會增大,會影響到系統的運行,假設有2個CPU的情況下,垃圾回收線程將會占據超過50%的CPU資源。所以,在選用CMS收集器的時候,需要考慮,當前的應用系統,是否對CPU資源敏感。

  其次,CMS收集器在處理垃圾收集的過程中,可能會產生浮動垃圾,由於它無法處理浮動垃圾,所以可能會出現Concurrent Mode Failure問題而導致觸發一次Full GC。所謂的浮動垃圾,是由於CMS收集器的並發清理階段,清理線程是和用戶線程一起運行,如果在清理過程中,用戶線程產生了垃圾對象,由於過了標記階段,所以這些垃圾對象就成為了浮動垃圾,CMS無法在當前垃圾收集過程中集中處理這些垃圾對象。由於這個原因,CMS收集器不能像其他收集器那樣等到完全填滿了老年代以后才進行垃圾收集,需要預留一部分空間來保證當出現浮動垃圾的時候可以有空間存放這些垃圾對象。在JDK 1.5中,默認當老年代使用了68%的時候會激活垃圾收集,這是一個保守的設置,如果在應用中老年代增長不是很快,可以通過參數"-XX:CMSInitiatingOccupancyFraction"控制觸發的百分比,以便降低內存回收次數來提供性能。在JDK 1.6中,CMS收集器的激活閥值變成了92%。如果在CMS運行期間沒有足夠的內存來存放浮動垃圾,那么就會導致"Concurrent Mode Failure"失敗,這個時候,虛擬機將啟動后備預案,臨時啟動Serial Old收集器來對老年代重新進行垃圾收集,這樣會導致垃圾收集的時間邊長,特別是當老年代內存很大的時候。所以對參數"-XX:CMSInitiatingOccupancyFraction"的設置,過高,會導致發生Concurrent Mode Failure,過低,則浪費內存空間。

  CMS的最后一個問題,就是它在進行垃圾收集時使用的"標記-清除"算法。上一篇文章介紹垃圾回收原理的時候,我們講到"標記-清除"算法,在進行垃圾清理以后,會出現很多內存碎片,過多的內存碎片會影響大對象的分配,會導致即使老年代內存還有很多空閑,但是由於過多的內存碎片,不得不提前觸發垃圾回收。為了解決這個問題,CMS收集器提供了一個"-XX:+UseCMSCompactAtFullCollection"參數,用於CMS收集器在必要的時候對內存碎片進行壓縮整理。由於內存碎片整理過程不是並發的,所以會導致停頓時間變長。"-XX:+UseCMSCompactAtFullCollection"參數默認是開啟的。虛擬機還提供了一個"-XX:CMSFullGCsBeforeCompaction"參數,來控制進行過多少次不壓縮的Full GC以后,進行一次帶壓縮的Full GC,默認值是0,表示每次在進行Full GC前都進行碎片整理。

  雖然CMS收集器存在上面提到的這些問題,但是毫無疑問,CMS當前仍然是非常優秀的垃圾收集器。

4. GC日志分析

  垃圾收集器在進行垃圾收集的過程中,可以輸出日志,我們通過日志,可以看到當前垃圾收集器的運行情況。通過gc日志,我們可以觀察垃圾收集器的行為,以及當前應用程序的GC情況和內存使用情況。學會查看和分析垃圾收集日志,一方面可以幫助我們學習垃圾收集器;另一方面,在必要的時候,可以幫助我們定位問題,解決問題,對JVM進行優化。

  默認,JVM不會打印出GC日志信息,可以通過參數-XX:+PrintGC或-verbose:gc來設置JVM輸出gc日志到終端中。

  JVM參數:-XX:+PrintGC -XX:+UseSerialGC -Xms10m -Xmx10m

[GC (Allocation Failure)  1922K->1394K(9920K), 0.0021245 secs] [Full GC (Allocation Failure) 7585K->7538K(9920K), 0.0023668 secs]

  當設置了"-XX:+PrintGC"或者"-verbose:gc"以后就會輸出類似輸出上面的GC日志。這是最簡單的GC日志,包含了垃圾收集過程中的信息。其中紅色部分的"GC"和"Full GC"表示這次GC的類型,而綠色部分的"Allocation Failure"表示表示發生這次GC的原因,從上面的日志可以看出,是由於內存分配失敗導致的GC。后面的黃色部分"1922K->1394K(9920K)"表示這次GC導致JVM中堆內存的使用量從1922K降低到了1394K,其中括號中表示當前整個JVM堆的大小。最后藍色部分的"0.0021245 secs"表示這次GC持續的時間。

  上面輸出的是簡單格式的GC日志,雖然提供了一些信息,但是通過這些信息,我們沒法知道這次GC發生的時候,這次GC是發生在老年代還是在年輕代,是否有對象從年輕代被移動到了老年代等信息,所以我們希望可以看到更加詳盡的信息。這個時候,我們需要設置-XX:+PrintGCDetails參數來輸出更加詳細的GC日志,下面我們結合不同的收集器組合,來分析下它們的輸出日志。

Serial GC + Serial Old

  Serial GC和Serial Old收集器是比較早的單線程收集器,工作原理我們在上面已經介紹過了。這里,我們來看下使用這兩款收集器進行垃圾收集的時候,輸出的日志格式是怎么樣的。首先我們需要設置JVM參數:

  JVM參數:-XX:+PrintGC -XX:+PrintGCDetails -XX:+UseSerialGC -Xms10m -Xmx10m

[GC (Allocation Failure) 
[DefNew: 1922K->319K(3072K), 0.0027356 secs] 1922K->1394K(9920K), 0.0027698 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure)
[Tenured: 6514K->6484K(6848K), 0.0025899 secs] 8562K->8532K(9920K), [Metaspace: 2984K->2984K(1056768K)], 0.0026153 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]

   可以發現,通過設置了"-XX:+PrintGCDetails"以后,輸出的GC日志信息多了很多。我們先來看第一條,紅色部分"GC"表示這次發生的是Minor GC,綠色部分"Allocation Failure"表示導致這次GC的原因是內存分配失敗。接下來,黃色部分的內容,則和前面的日志有些區別了,這里輸出的內容相對比較詳細。"DefNew: 1922K->319K(3072K), 0.0027356 secs] 1922K->1394K(9920K), 0.0027698 secs",其中DefNew表示這次GC發生在年輕代(不同的收集器,日志的格式不一定相同),接下來"1922K->319K"表示這次GC導致年輕代使用的內存從1922K降到319K,括號中的"3072K"表示年輕代中的堆內存大小為3072K。"0.0027356 secs"表示這次年輕代GC耗時0.0027356s。后面的"1922K->1393K"表示總的堆內存(年輕代 + 老年代)的使用情況的變化,從1922K降低到1394K, 括號中的"9920K"表示總的堆內存的大小。最后的"0.0027698 secs"表示這次GC總的消耗的時間。最后是這次GC消耗的時間的統計,其中user表示用戶態CPU執行的時間,sys表示內核態CPU執行的時間,這兩個時間不包括被掛起消耗的時間,而real表示的是實際的時間,可以認為是牆上時鍾走過的時間。

  下面的這條日志,"Full GC"表示這次GC是一次Major GC,后面的原因和上面一樣。我們來看下黃色部分,"Tenured"表示這次GC發生在老年代,其中"6524K->6484K"表示老年代內存從6524K降低到6484K。后面的時間"0.0025899 secs"表示這次老年代GC耗時0.0025899s。接下來的"8562K -> 8532K"和上面提到的一樣,表示整個堆內存的變化。最后的時間表示這次GC的總耗時為"0.0026153s"。

Parallel Scanvage + Parallel Old

  不同的垃圾收集器,輸出的日志信息也不是完全相同的,上面我們看到的日志,是使用Serial GC和Serial Old收集器輸出的gc日志,而下面的日志信息,則是使用Parallel Scavenge收集器和Parallel Old收集器輸出的日志。

  JVM參數:-XX:+PrintGC -XX:+UseParallelOldGC -XX:+PrintGCDetails -Xms10m -Xmx10m

[GC (Allocation Failure) --
[PSYoungGen: 1391K->1391K(2560K)] 7537K->7537K(9728K), 0.0007436 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure)
[PSYoungGen: 1391K->1374K(2560K)]
[ParOldGen: 6145K->6145K(7168K)] 7537K->7520K(9728K), [Metaspace: 2984K->2984K(1056768K)], 0.0037697 secs]
[Times: user=0.01 sys=0.00, real=0.01 secs]

  可以看到,使用Parallel Scavenge 和 Parallel Old收集器輸出的日志,會有一些不同,不過日志內容大體上差不多。最后,我們來看下CMS垃圾收集器的日志是怎么樣的,相對上面幾款收集器,CMS相對更加復雜,從它輸出的日志也可以看出來。

ParNew + Concurrent Mark Sweep(CMS)

  下面,我們來看下ParNew配合CMS收集器在進行垃圾收集的時候,輸出的GC 日志信息。

  JVM參數:-XX:+PrintGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms10m -Xmx10m

[GC (Allocation Failure) [ParNew: 2418K->0K(3072K), 0.0032236 secs] 3508K->3455K(9920K), 0.0032520 secs] 
[Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (CMS Initial Mark) [1 CMS-initial-mark: 3455K(6848K)] 4479K(9920K), 0.0005566 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [CMS-concurrent-mark-start] [CMS-concurrent-mark: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [CMS-concurrent-preclean-start] [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (CMS Final Remark) [YG occupancy: 1024 K (3072 K)]
[Rescan (parallel) , 0.0001118 secs][weak refs processing, 0.0000191 secs][class unloading, 0.0002858 secs]
[scrub symbol table, 0.0003506 secs][scrub string table, 0.0001305 secs]
[1 CMS-remark: 3455K(6848K)] 4479K(9920K), 0.0009500 secs]
[Times: user=0.00 sys=0.00, real=0.01 secs] [CMS-concurrent-sweep-start] [CMS-concurrent-sweep: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [CMS-concurrent-reset-start] [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

  通過第一條日志,可以看出我們使用"-XX:+UseConcMarkSweepGC"指定CMS垃圾收集器的時候,使用的是ParNew + CMS收集器組合。下面輸出的一堆日志,就是CMS收集器在進行垃圾收集過程中輸出的信息。可以明顯的看到,CMS在進行垃圾收集的過程中,經歷了4個階段,在日志中我用4中顏色標記出來了。需要注意的是黃色部分,這是CMS的重新標記的階段,在上面我們介紹CMS收集器的時候說過,在這個階段,是會出現Stop The World的,所以如果這個階段消耗的時間比較長,則會影響應用的響應時間。

其他日志參數

  有時候,我們需要在GC日志中輸出時間值,這樣我們就可以知道這次GC發生的具體時間點。我們可以通過JVM參數"-XX:+PrintGCTimeStamps" 和"-XX:+PrintGCDateStamps"來設置日志輸出的時間。使用"-XX:+PrintGCTimeStamps"參數,可以在輸出的日志前加上產生日志的時間戳:

7.327: [GC (Allocation Failure) 7.327: [DefNew: 2095K->2095K(3072K), 0.0000209 secs]

  可以看到,輸出的日志中,在頭部包含了一個時間戳,表示從JVM啟動以來經過的秒數。而"-XX:+PrintGCDateStamps"則表示輸出日志時的當前時間,相對來說更加直觀:

2017-02-24T00:14:38.611-0800: [GC (Allocation Failure) 
2017-02-24T00:14:38.611-0800: [DefNew: 1922K->319K(3072K), 0.0025676 secs] 1922K->1394K(9920K), 0.0026134 secs]

  除了將日志輸出到控制台,我們還可以將日志輸出到日志文件中,這樣就可以通過分析日志文件來分析系統的GC情況了,一般在服務器運行過程中,我們都會將GC日志輸出到指定的文件中,供需要的時候分析。可以通過JVM參數"-Xloggc:<file>"來指定日志輸出的目錄。  

5. 總結

  在這篇文章中,我們討論了現代Java虛擬機中已經實現了的垃圾收集器。從分代收集策略出發,結合上一篇文章中介紹的垃圾收集原理,介紹了多款垃圾收集器的是實現。最后,我們分析了垃圾收集器的GC日志,學習如何通過垃圾收集的日志,分析當前系統的垃圾收集的狀況。文章到這里差不多就介紹, 希望這篇文章可以幫助到大家!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM