1. 程式人生 > >JVM(三) 垃圾回收時間點和垃圾收集器

JVM(三) 垃圾回收時間點和垃圾收集器

        收集器組合章節來自第一篇參考文章,非原創,作者總結地非常好!

         分代收集相關概念來自參考文章第二篇,非原創

        第二篇參考資料的文章質量很高,推薦閱讀!

分代收集(Generational Collection)相關概念

      在Java8的HotSpot虛擬機器中一共包括了5個垃圾收集器,它們每一個都是基於分代收集的思想。在這一節中,我主要介紹一下各個分代區域以及物件是怎樣被分配到這些區域的。這是官方文件給出的5個可得到的收集器:

5 Available Collectors,並介紹瞭如何針對自己的應用選擇出一個合適的收集器。

 

Generational Hypothesis

        對於Generational Hypothesis的概念,Jeff Hammerbacher在Quora上已經給出一個很好地答案,我把它翻譯一下。

Generational Hypothesis是一個關於物件生命週期分佈的假設。準確地說,這個假設認為物件生命週期的分佈是雙峰的:大部分的物件要麼是短命的,要麼就是一直存活的。

Generational Hypothesis

Generational Hypothesis

       基於這個假設,Hotspot虛擬機器把記憶體分為年輕代(Young Generation)和老年代(Old Generation)。有了這樣的記憶體區域劃分,我們可以針對不同的區域選擇合適的演算法來進行垃圾收集,從而大大提高垃圾收集的效率。注意:分代收集是基於上面的假設來進行的,如果你的應用完全不符合上面的假設,那麼你的垃圾收集效率一定很低。

       因為年輕代空間通常很小,包含很多短命的物件,所以它的收集要頻繁一些。經過了幾輪年輕代收集,依然存活的物件被晉升(promoted)或者tenured到老年代。因為老年代的空間要比年輕代大很多並且它的物件大部分都是長命的,所以它的收集是不頻繁的。由於年輕代的收集很頻繁,因此針對這個區域的收集演算法要很快。另一方面,由於老年代的收集不是很頻繁的並且它佔用了大多數的堆空間,因此這一區域的演算法針對低頻的垃圾收集要空間有效的。

        在介紹各個分代區域之前,大家先看看下面這張圖。

Generational Collection

Generational Collection

注意:在Java 8中已經移除了永久代。

 

年輕代

      年輕代是由一個Eden區域 + 2個survivor區域組成。大部分的物件最初都被分到Eden區域(特別大的物件可能直接被分配到老年代)。對於2個survivor區域來說,它們中的一個必須始終是空的。並且每個survivor區域中的物件至少是經歷過一次年輕代垃圾收集的。假設幾分鐘前垃圾收集器已經進行了一次年輕代的垃圾收集了,Eden區域和其中的1個survivor區域都有物件,另一個survivor區域為空。現在,又要進行一次垃圾收集了,收集器做的就是:把Eden區域和那個有物件的survivor區域中活著的物件找出來並複製到另一個空的survivor區域中,然後清空Eden區域和先前有物件的那個survivor區域。

       如果空的這個survivor區域的空間不夠裝下Eden區域和另一個survivor區域中活著的物件,那麼收集器會把容納不下的物件直接分配到老年代。如果老年代也容不下這些物件,那麼會觸發老年代的垃圾收集,然後去容納這些物件。

        由於Java應用支援多執行緒,那麼在多執行緒應用的情況下物件的分配就會出現一些問題。比如,我上一個執行緒分配的物件會被下一個執行緒所分配的物件覆蓋。如果用一個全域性鎖來把整個年輕代鎖住,那麼分配一個物件到年輕代的效能會非常低下。因此,虛擬機器團隊想出了一個解決方案叫做Thread-Local Allocation Buffers (TLABs).

 

Thread-Local Allocation Buffers

Thread-Local Allocation Buffers

       如上圖所示,每一個執行緒都有一個自己的TLAB,分配物件時用指標碰撞(bump-the-pointer)技術去在各自的TLAB中分配物件,大大提升了年輕代分配物件的效率。設定‐XX:+UseTLAB來啟用TLAB,通過‐XX:TLABSize來設定其大小,預設大小為0,0表示虛擬機器動態計算其大小。

       經過了幾次垃圾收集還沒有被回收的物件就被promoted到老年代了。那麼如何去判斷一個物件是否足夠老可以晉升到老年代呢?垃圾收集器追蹤每個活著物件被收集的次數,每挺過一次垃圾收集,物件的年齡就加1,當一個物件的年齡超過了指定的闕值(tenuring threshold),它就會被晉升到老年代。通過設定XX:+MaxTenuringThreshold來指定一個上限,如果設定為0,那麼經過1次垃圾收集以後馬上被晉升。

 

老年代

      老年代的空間是非常大的並且它裡面存在的物件成為垃圾的可能性很小。老年代的垃圾收集次數要比年輕代少很多,並且由於老年代的物件很少會成為垃圾物件,年輕代的做法(在survivor區域不斷copy)並不適合老年代。老年代具體的收集演算法我會在下面具體的垃圾收集器中介紹。

 

永久代

        永久代在Java 8以前存在。 JVM用這裡儲存一些類的元資料還有一些被內在化的字串。What is String interning?詳細地解釋了什麼是內在化字串。Hotspot虛擬機器用永久代實現了方法區,因此如果你用動態代理技術或CGLib產生大量的增強代理類,都會使永久代出現異常。比如,當你用Spring的AOP時,它都會為想要增強的類產生一個代理類從而達到增強的目的,如果產生的類很多,你的永久代將會溢位。

永久代給Java開發者製造了很多的麻煩,因為很難預測出它將需要多少記憶體空間。如果出現溢位:產生java.lang.OutOfMemoryError: Permgen space.的錯誤。

 

Metaspace

        由於上面永久代的缺點,它在Java 8中被移除,取而代之的是Metaspace,這塊記憶體區域位於本地記憶體中。預設情況下,Metaspace的大小隻被Java程序可得到的本地記憶體所限制。因此,這個區域並不會因為稍微增加一個類就導致溢位。注意:Metaspace沒有限制地增長將會導致本地記憶體溢位。 你可以設定-XX:MaxMetaspaceSize來限制其大小。

 

HotSpot 垃圾回收時間點

列舉根節點

       垃圾回收必須要列舉到根節點經過判斷才可以知道哪些物件是存活的,於是就會有以下的問題了:

  • 根節點那麼多,逐個檢查需要時間,用什麼來記錄需要回收的根節點和相關的引用,即列舉根節點
  • 多個執行緒必須在同個時間點停下,接受垃圾回收,這個時間點應該在哪裡呢
  • 應該如何中斷正在執行的執行緒,當要GC 時所有執行緒一起停下來嗎?
  • 有的執行緒是Sleep 或是 Blocked 狀態時,收不到GC 發過來的標誌訊號,那怎麼辦呢?

        第一個問題,JVM 使用了一組稱為OopMap的資料結構來達到這個目的,在類載入完成的時候,HotSpot 就把物件記憶體多少偏移量上是什麼型別的資料計算出來(有點像java中的unsafe類的方法,在AQS中或是concurrenthashmap中),在JIT編譯過程中在特定位置紀錄下棧和暫存器中哪些位置是引用。

        第二個問題,這個時間點稱之為 “Stop The World”----Safepoint,這個時間點在程式中的選定要是太少,那麼GC 就要等待太久,要是太多個安全點,那麼GC 次數變多,容易引起效能問題,所以安全點的選定基本上是以“是否具有讓程式長時間執行的特徵”,具有這類的最明顯的特徵就是指令序列複用,例如方法呼叫,迴圈跳轉,異常跳轉等。

        第三個問題, JVM 使用主動式中斷( Voluntary Suspension ),當要GC時,僅僅設定一個標誌位,讓執行緒自己去主動輪詢這個標誌,發現中斷標誌為真時,就自己掛起,輪徐標誌的地方和安全點是重合的。

        第四個問題,JVM設定了一個“安全區域(Safe Region)”,即在這個範圍內,引用關係不會裱花,我們也可以把Safe Region 看做是被擴充套件的 Safe Point.

 

垃圾收集型別

由於Hotspot虛擬機器的垃圾收集是基於分代思想的,那麼在不同的分代區域收集會產生不同的垃圾收集型別,本節我將會介紹這些垃圾收集型別以及它們發生的時機。

 

minor gc

發生在年輕代的垃圾收集叫做minor gc,它具體的細節是什麼樣呢?

  • 當JVM不能為一個新物件分配空間時,minor gc被觸發。例如:Eden區域被填滿時。因此,你的應用分配物件的頻率越高,minor gc發生的越頻繁。
  • 在minor gc期間,老年代實際上被忽略。因此,從老年代到年輕代的引用被當作GC roots,而從年輕代到老年代的引用在標記階段被忽略。
  • minor gc會觸發stop-the-world的發生,致使應用執行緒停止。如果在Eden區域中的大部分物件都被標記為垃圾,既符合上面的假設,那麼停頓時間是可以忽略不計的。但是,如果與假設相反,在Eden區域依然大部分的物件都是活著的,那麼minor gc會花費很多的時間。

 

full gc

        清理整個堆的過程叫做full gc,有時也叫做major collection. 當老年代太滿了而不能要接受所有來自年輕代晉升的物件時,所有的收集器(除了CMS)將停止年輕代的收集演算法執行,而是用老年代的收集演算法清理整個堆記憶體。(CMS垃圾收集器的老年代收集演算法不能收集年輕代)。

  

垃圾收集器

         先上一張圖。

_thumb

 

概述

七種垃圾收集器

  1. Serial(序列GC)-複製
  2. ParNew(並行GC)-複製
  3. Parallel Scavenge(並行回收GC)-複製
  4. Serial Old(MSC)(序列GC)-標記-整理
  5. CMS(併發GC)-標記-清除
  6. Parallel Old(並行GC)--標記-整理
  7. G1(JDK1.7update14才可以正式商用)

說明:

  1. 1~3用於年輕代垃圾回收:年輕代的垃圾回收稱為minor GC
  2. 4~6用於年老代垃圾回收(當然也可以用於方法區的回收):年老代的垃圾回收稱為full GC
  3. G1獨立完成"分代垃圾回收"

注意:並行與併發

  1. 並行:多條垃圾回收執行緒同時操作
  2. 併發:垃圾回收執行緒與使用者執行緒一起操作

 

常用五種組合

  1. Serial/Serial Old
  2. ParNew/Serial Old:與上邊相比,只是比年輕代多了多執行緒垃圾回收而已
  3. ParNew/CMS:當下比較高效的組合
  4. Parallel Scavenge/Parallel Old:自動管理的組合
  5. G1:最先進的收集器,但是需要JDK1.7update14以上

 

Serial 和 Serial Old 收集器

         從圖中可以看出,前者在新生代,後者在老生代,Serial只有一個執行緒去收集,並且必須暫停其他所有的工作執行緒,知道它收集結束。優點是簡單和高效,缺點是不夠靈活,適合執行在 Client 模式下的虛擬機器。

   Serial Old 就是 Serial 在老生代的版本,也是適合執行在 Client 模式下的虛擬機器。要是執行在Server 模式下, Serial Old 有以下的作用 :

 

  • 在 JDK 1.5以及之前的版本中與 Parallel Scavenage 收集器搭配使用
  • 作為CMS 收集器的後備預案,在併發收集發生Current Mode Failure 時使用

 

parNew 收集器

      它存在在新生代,就是Serial收集器的多版本。

     

     

Paranlle Scavenge 和 Paranlle Old 收集器

          Paranlle Scavenge 是一個新生代收集器,也是使用複製演算法被稱為“吞吐量優先”的收集器,它的特點有兩點 :

 

  • 實現一個可控制的的吞吐量(Throughput)
  • GC 自適應的調節策略(GC Ergonomics)


          第一個特點,吞吐量就是執行使用者程式碼時間佔(執行使用者程式碼時間+垃圾收集時間)的比值,可以通過引數調整吞吐量。Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,說明CPU利用率越高,所以主要用於處理很多的CPU計算任務而使用者互動任務較少的情況)
          第二個特點,JVM根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量。

          Paranlle Old 是 Paranlle Scavenge 的老年代版本,使用多執行緒和“標記-整理”演算法。在注重吞吐量以及CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 組合的收集器。

 

CMS(Concurrent Mark Sweep) 收集器

       CMS收集器是一種以獲取最短回收停頓時間為目標的基於“標記-清除”的收集器。

       CMS Collector也叫做low-latency collector,它是專門為老年代設計的。因為年輕代的stop-the-world時間不會太長,而對於老年代來說,雖然它的收集並不頻繁,但是每次收集都可能會出現較長的停頓時間,尤其是在堆空間很大的時候。而CMS Collector的出世就是解決老年代停頓時間長的問題。解決這個問題它主要通過下面2個手段:

 

  1. 當老年代收集過後,CMS Collector並不會去compacting老年代,而是用空閒列表(free-lists)去管理被釋放的空間。
  2. 它在mark-and-sweep階段大部分的時候都是與我們自己的應用併發執行。

 

回收過程分四個步驟 :

 

  • 初始標記(CMS inital mark):標記與根集合節點直接關聯的節點。時間非常短,需要STW
  • 併發標記(CMS concurrent mark):遍歷之前標記到的關聯節點,繼續向下標記所有存活節點。時間較長。
  • 重新標記(CMS remark):重新遍歷trace併發期間修改過的引用關係物件。時間介於初始標記與併發標記之間,通常不會很長。需要STW 所以可以知道併發標記的時間最長的併發標記,重新標誌只是修改併發期間發生的變化的節點
  • 併發清理(CMS concurrent sweep):直接清除非存活物件,清理之後,將該執行緒佔用的CPU切換給使用者執行緒

 

缺點 :

  1. CMS 收集器對CPU 資源非常敏感
  2. CMS 收集器無法處理浮動垃圾(Floating Garbage),繼而可能出現“Concurrent Mode Failure”失敗而導致另一次
      Full GC 的產生。

CMS 併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾
   出現在標記過程之後,CMS 無法在當次收集中處理掉他們,這部分垃圾就是浮動垃圾。

 

      也是由於在垃圾收集階段使用者執行緒還需要執行,那也就還需要預留有足夠的記憶體空間用使用者執行緒使用, 因此CMS 收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了在收集,CMS會預留一部分空間提供併發收集時的程式執行使用。要是預留的空間無法程式需要(浮動垃圾太多,需要回收的東西太多),就會出現一次“Concurrent Mode Failure” 失敗,這時虛擬機器將啟動後備預案 : 臨時啟用 Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

 

   3.    CMS 是基於“標記-清除”演算法,空間碎片過多,可能出現無法分配大物件的情況,往往會出現老年代還有很大空間剩餘,但無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次 Full GC

 

G1 收集器

       可以獨立執行,下面總結一下它的特點 :

  • 並行與併發 : 能充分利用多CPU來縮短 Stop-The-World 的時間
  • 分代收集 : 分代概念依舊得以保留
  • 空間整合 : 使用為區域定為“Region”的方法,保證了執行期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體,那麼也就不會存在大物件無法分配的情況了。
  • 可預測的停頓

 

原理:

  • G1收集器將整個堆劃分為多個大小相等的Region,每個Region 都有個Remembered Set 來避免全堆掃描的,G1 中每個Region 都有一個與之對應的Remembered Set
  • G1跟蹤各個region裡面的垃圾堆積的價值(回收後所獲得的空間大小以及回收所需時間長短的經驗值),在後臺維護一張優先列表,每次根據允許的收集時間,優先回收價值最大的region,這種思路:在指定的時間內,掃描部分最有價值的region(而不是掃描整個堆記憶體),並回收,做到儘可能的在有限的時間內獲取儘可能高的收集效率。

 

 

收集器組合

Serial/Serial Old:

1_thumb

特點:

  • 年輕代Serial收集器採用單個GC執行緒實現"複製"演算法(包括掃描、複製)
  • 年老代Serial Old收集器採用單個GC執行緒實現"標記-整理"演算法
  • Serial與Serial Old都會暫停所有使用者執行緒(即STW)

 

適用場合:

  • CPU核數<2,實體記憶體<2G的機器(簡單來講,單CPU,新生代空間較小且對STW時間要求不高的情況下使用)
  • -XX:UseSerialGC:強制使用該GC組合
  • -XX:PrintGCApplicationStoppedTime:檢視STW時間
  • 由於它實現相對簡單,沒有執行緒相關的額外開銷(主要指執行緒切換與同步),因此非常適合運行於客戶端PC的小型應用程式,或者桌面應用程式(比如swing編寫的使用者介面程式),以及我們平時的開發、除錯、測試等。

 


 

ParNew/Serial Old:

2_thumb

說明:

ParNew除了採用多GC執行緒來實現複製演算法以外,其他都與Serial一樣,但是此組合中的Serial Old又是一個單GC執行緒,所以該組合是一個比較尷尬的組合,在單CPU情況下沒有Serial/Serial Old速度快(因為ParNew多執行緒需要切換),在多CPU情況下又沒有之後的三種組合快(因為Serial Old是單GC執行緒),所以使用其實不多。

-XX:ParallelGCThreads:指定ParNew GC執行緒的數量,預設與CPU核數相同,該引數在於CMS GC組合時,也可能會用到

 


 

Parallel Scavenge/Parallel Old:

3_thumb

 

特點:

  1. 年輕代Parallel Scavenge收集器採用多個GC執行緒實現"複製"演算法(包括掃描、複製)
  2. 年老代Parallel Old收集器採用多個GC執行緒實現"標記-整理"演算法
  3. Parallel Scavenge與Parallel Old都會暫停所有使用者執行緒(即STW)

 

說明:

  1. 吞吐量:CPU執行程式碼時間/(CPU執行程式碼時間+GC時間)
  2. CMS主要注重STW的縮短(該時間越短,使用者體驗越好,所以主要用於處理很多的互動任務的情況)
  3. Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,說明CPU利用率越高,所以主要用於處理很多的CPU計算任務而使用者互動任務較少的情況)

 

引數設定:

  1. -XX:+UseParallelOldGC:使用該GC組合
  2. -XX:GCTimeRatio:直接設定吞吐量大小,假設設為19,則允許的最大GC時間佔總時間的1/(1 +19),預設值為99,即1/(1+99)
  3. -XX:MaxGCPauseMillis:最大GC停頓時間,該引數並非越小越好
  4. -XX:+UseAdaptiveSizePolicy:開啟該引數,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold這些引數就不起作用了,虛擬機器會自動收集監控資訊,動態調整這些引數以提供最合適的的停頓時間或者最大的吞吐量(GC自適應調節策略),而我們需要設定的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio兩個引數就好(當然-Xms也指定上與-Xmx相同就好)

 

適用場合:

  1. 很多的CPU計算任務而使用者互動任務較少的情況
  2. 不想自己去過多的關注GC引數,想讓虛擬機器自己進行調優工作
  3. 對吞吐量要求較高,或需要達到一定的量。

 


 

ParNew/CMS:

4_thumb

 

說明:

  1. 以上只是年老代CMS收集的過程,年輕代ParNew看"2.2、ParNew/Serial Old"就好
  2. CMS是多回收執行緒的,不要被上圖誤導,預設的執行緒數:(CPU數量+3)/4
  3. CMS主要注重STW的縮短(該時間越短,使用者體驗越好,所以主要用於處理很多的互動任務的情況)

 

特點:

1.年輕代ParNew收集器採用多個GC執行緒實現"複製"演算法(包括掃描、複製)

2.年老代CMS收集器採用多執行緒實現"標記-清除"演算法,整個過程分4個步驟,上面已介紹了

3.初始標記重新標記都會暫停所有使用者執行緒(即STW),但是時間較短;併發標記與併發清理時間較長,但是不需要STW

關於併發標記期間怎樣記錄發生變動的引用關係物件,在重新標記期間怎樣掃描這些物件

 

引數設定:

  • -XX:+UseConcMarkSweepGC:使用該GC組合
  • -XX:CMSInitiatingOccupancyFraction:指定當年老代空間滿了多少後進行垃圾回收
  • -XX:+UseCMSCompactAtFullCollection:(預設是開啟的)在CMS收集器頂不住要進行FullGC時開啟記憶體碎片整理過程,該過程需要STW
  • -XX:CMSFullGCsBeforeCompaction:指定多少次FullGC後才進行整理
  • -XX:ParallelCMSThreads:指定CMS回收執行緒的數量,預設為:(CPU數量+3)/4

 

適用場合:

用於處理很多的互動任務的情況

方法區的回收一般使用CMS,配置兩個引數:-XX:+CMSPermGenSweepingEnabled與-XX:+CMSClassUnloadingEnabled

適用於一些需要長期執行且對相應時間有一定要求的後臺程式

 


 

G1

5_thumb

 

說明:

  • 從上圖來看,G1與CMS相比,僅在最後的"篩選回收"部分不同(CMS是併發清除),實際上G1回收器的整個堆記憶體的劃分都與其他收集器不同。
  • CMS需要配合ParNew,G1可單獨回收整個空間

 

 

運作流程:

  • 初始標記:標記出所有與根節點直接關聯引用物件。需要STW
  • 併發標記:遍歷之前標記到的關聯節點,繼續向下標記所有存活節點。在此期間所有變化引用關係的物件,都會被記錄在Remember Set Logs中
  • 最終標記:標記在併發標記期間,新產生的垃圾。需要STW
  • 篩選回收:根據使用者指定的期望回收時間回收價值較大的物件(看"原理"第二條)。需要STW

 

優點:

  1. 停頓時間可以預測:我們指定時間,在指定時間內只回收部分價值最大的空間,而CMS需要掃描整個年老代,無法預測停頓時間
  2. 無記憶體碎片:垃圾回收後會整合空間,CMS採用"標記-清理"演算法,存在記憶體碎片
  3. 篩選回收階段:
  • 由於只回收部分region,所以STW時間我們可控,所以不需要與使用者執行緒併發爭搶CPU資源,而CMS併發清理需要佔據一部分的CPU,會降低吞吐量。
  • 由於STW,所以不會產生"浮動垃圾"(即CMS在併發清理階段產生的無法回收的垃圾)

 

適用範圍:

  • 追求STW短:若ParNew/CMS用的挺好,就用這個;若不符合,用G1
  • 追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面沒有優勢

    

參考資料 :