學習JVM-GC收集器

1. 前言

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

  介紹完原理,在這篇文章中,咱們將介紹當前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將不能動態擴展堆內存,它的初始堆內存和最大堆內存是相同的。spa

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日誌,學習如何經過垃圾收集的日誌,分析當前系統的垃圾收集的情況。文章到這裏差很少就介紹, 但願這篇文章能夠幫助到你們!

相關文章
相關標籤/搜索