JVM垃圾回收算法及分代垃圾收集器


一、垃圾收集器的分類

1、次收集器  

  Scavenge GC,指發生在新生代的GC,因為新生代的Java對象大多都是朝生夕死,所以Scavenge GC非常頻繁,一般回收速度也比較快。當Eden空間不足以為對象分配內存時,會觸發Scavenge GC。  

  一般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然后整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這里需要使用速度快、效率高的算法,使Eden去能盡快空閑出來。

  當年輕代堆空間緊張時會被觸發,相對於全收集而言,收集間隔較短。

2、全收集器

  Full GC,指發生在老年代的GC,出現了Full GC一般會伴隨着至少一次的Minor GC(老年代的對象大部分是Scavenge GC過程中從新生代進入老年代),比如:分配擔保失敗。Full GC的速度一般會比Scavenge GC慢10倍以上。當老年代內存不足或者顯式調用System.gc()方法時,會觸發Full GC。
  當老年代或者持久代堆空間滿了,會觸發全收集操作。可以使用System.gc()方法來顯式的啟動全收集,全收集一般根據堆大小的不同,需要的時間不盡相同,但一般會比較長。

3、垃圾回收器的常規匹配

二、常見垃圾回收算法

1、引用計數(Reference Counting)

  比較古老的回收算法。原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的對象。此算法最致命的是無法處理循環引用的問題。

2、復制(Copying)

  此算法把內存空間划為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的對象復制到另外一個區域中。此算法每次只處理正在使用中的對象,因此復制成本比較小,同時復制過去以后還能進行相應的內存整理,不會出現“碎片”問題。當然,此算法的缺點也是很明顯的,就是需要兩倍內存空間。簡圖如下:

3、標記-清除(Mark-Sweep)

  此算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的對象,第二階段遍歷整個堆,把未標記的對象清除。此算法需要暫停整個應用,同時,會產生內存碎片。簡圖如下:

4、標記-整理(Mark-Compact)

  此算法結合了“標記-清除”和“復制”兩個算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用對象,第二階段遍歷整個堆,把清除未標記對象並且把存活對象“壓縮”到堆的其中一塊,按順序排放。此算法避免了“標記-清除”的碎片問題,同時也避免了“復制”算法的空間問題。簡圖如下:

三、分代垃圾收集器

1、串行收集器(Serial)

  Serial收集器是Hotspot運行在Client模式下的默認新生代收集器, 它的特點是:只用一個CPU(計算核心)/一條收集線程去完成GC工作, 且在進行垃圾收集時必須暫停其他所有的工作線程(“Stop The World” -后面簡稱STW)。可以使用-XX:+UseSerialGC打開。 雖然是單線程收集, 但它卻簡單而高效, 在VM管理內存不大的情況下(收集幾十M~一兩百M的新生代), 停頓時間完全可以控制在幾十毫秒~一百多毫秒內。

2、並行收集器(ParNew)

  ParNew收集器其實是前面Serial的多線程版本, 除使用多條線程進行GC外, 包括Serial可用的所有控制參數、收集算法、STW、對象分配規則、回收策略等都與Serial完全一樣(也是VM啟用CMS收集器-XX: +UseConcMarkSweepGC的默認新生代收集器)。

  由於存在線程切換的開銷, ParNew在單CPU的環境中比不上Serial, 且在通過超線程技術實現的兩個CPU的環境中也不能100%保證能超越Serial. 但隨着可用的CPU數量的增加, 收集效率肯定也會大大增加(ParNew收集線程數與CPU的數量相同, 因此在CPU數量過大的環境中, 可用-XX:ParallelGCThreads=<N>參數控制GC線程數)。

3、Parallel Scavenge收集器

  與ParNew類似, Parallel Scavenge也是使用復制算法, 也是並行多線程收集器. 但與其他收集器關注盡可能縮短垃圾收集時間不同, Parallel Scavenge更關注系統吞吐量:
  系統吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。
  停頓時間越短就越適用於用戶交互的程序-良好的響應速度能提升用戶的體驗;而高吞吐量則適用於后台運算而不需要太多交互的任務-可以最高效率地利用CPU時間,盡快地完成程序的運算任務. Parallel Scavenge提供了如下參數設置系統吞吐量:

Parallel Scavenge參數

描述

-XX:MaxGCPauseMillis

(毫秒數) 收集器將盡力保證內存回收花費的時間不超過設定值, 但如果太小將會導致GC的頻率增加.

-XX:GCTimeRatio

(整數:0 < GCTimeRatio < 100) 是垃圾收集時間占總時間的比率

XX:+UseAdaptiveSizePolicy

啟用GC自適應的調節策略: 不再需要手工指定-Xmn-XX:SurvivorRatio-XX:PretenureSizeThreshold等細節參數, VM會根據當前系統的運行情況收集性能監控信息, 動態調整這些參數以提供最合適的停頓時間或最大的吞吐量

4、Serial Old收集器

  Serial Old是Serial收集器的老年代版本, 同樣是單線程收集器,使用“標記-整理”算法。

5、Parallel Old收集器

  Parallel Old是Parallel Scavenge收集器的老年代版本, 使用多線程和“標記-整理”算法, 吞吐量優先, 主要與Parallel Scavenge配合在注重吞吐量及CPU資源敏感系統內使用;

6、CMS收集器(Concurrent Mark Sweep)

  CMS(Concurrent Mark Sweep)收集器是一款具有划時代意義的收集器, 一款真正意義上的並發收集器, 雖然現在已經有了理論意義上表現更好的G1收集器, 但現在主流互聯網企業線上選用的仍是CMS(如Taobao、微店).
  CMS是一種以獲取最短回收停頓時間為目標的收集器(CMS又稱多並發低暫停的收集器), 基於”標記-清除”算法實現, 整個GC過程分為以下4個步驟:
1. 初始標記(CMS initial mark)
2. 並發標記(CMS concurrent mark: GC Roots Tracing過程)
3. 重新標記(CMS remark)
4. 並發清除(CMS concurrent sweep: 已死對象將會就地釋放, 注意:此處沒有壓縮)
  其中1,3兩個步驟(初始標記、重新標記)仍需STW. 但初始標記僅只標記一下GC Roots能直接關聯到的對象, 速度很快; 而重新標記則是為了修正並發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄, 雖然一般比初始標記階段稍長, 但要遠小於並發標記時間.

CMS特點:
  1. CMS默認啟動的回收線程數=(CPU數目+3)/4,當CPU數>4時, GC線程一般占用不超過25%的CPU資源, 但是當CPU數<=4時, GC線程可能就會過多的占用用戶CPU資源, 從而導致應用程序變慢, 總吞吐量降低.
  2.無法處理浮動垃圾, 可能出現Promotion Failure、Concurrent Mode Failure而導致另一次Full GC的產生: 浮動垃圾是指在CMS並發清理階段用戶線程運行而產生的新垃圾. 由於在GC階段用戶線程還需運行, 因此還需要預留足夠的內存空間給用戶線程使用, 導致CMS不能像其他收集器那樣等到老年代幾乎填滿了再進行收集. 因此CMS提供了-XX:CMSInitiatingOccupancyFraction參數來設置GC的觸發百分比(以及-XX:+UseCMSInitiatingOccupancyOnly來啟用該觸發百分比), 當老年代的使用空間超過該比例后CMS就會被觸發(JDK 1.6之后默認92%). 但當CMS運行期間預留的內存無法滿足程序需要, 就會出現上述Promotion Failure等失敗, 這時VM將啟動后備預案: 臨時啟用Serial Old收集器來重新執行Full GC(CMS通常配合大內存使用, 一旦大內存轉入串行的Serial GC, 那停頓的時間就是大家都不願看到的了).
  3.最后, 由於CMS采用”標記-清除”算法實現, 可能會產生大量內存碎片. 內存碎片過多可能會導致無法分配大對象而提前觸發Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection開關參數, 用於在Full GC后再執行一個碎片整理過程. 但內存整理是無法並發的, 內存碎片問題雖然沒有了, 但停頓時間也因此變長了, 因此CMS還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction用於設置在執行N次不進行內存整理的Full GC后, 跟着來一次帶整理的(默認為0: 每次進入Full GC時都進行碎片整理).

7、分區收集- G1收集器

  G1(Garbage-First)是一款面向服務端應用的收集器, 主要目標用於配備多顆CPU的服務器治理大內存.
  - G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
  -XX:+UseG1GC啟用G1收集器.
  與其他基於分代的收集器不同, G1將整個Java堆划分為多個大小相等的獨立區域(Region), 雖然還保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔離的了, 它們都是一部分Region(不需要連續)的集合.如:

  每塊區域既有可能屬於O區、也有可能是Y區, 因此不需要一次就對整個老年代/新生代回收. 而是當線程並發尋找可回收的對象時, 有些區塊包含可回收的對象要比其他區塊多很多. 雖然在清理這些區塊時G1仍然需要暫停應用線程, 但可以用相對較少的時間優先回收垃圾較多的Region. 這種方式保證了G1可以在有限的時間內獲取盡可能高的收集效率.
  G1的新生代收集跟ParNew類似: 存活的對象被轉移到一個/多個Survivor Regions. 如果存活時間達到閥值, 這部分對象就會被提升到老年代.如圖:

其特定是:
  一整塊堆內存被分為多個Regions.存活對象被拷貝到新的Survivor區或老年代.年輕代內存由一組不連續的heap區組成, 這種方法使得可以動態調整各代區域尺寸.Young GC會有STW事件, 進行時所有應用程序線程都會被暫停.
多線程並發GC.
G1老年代GC特點如下:
並發標記階段
  1.在與應用程序並發執行的過程中會計算活躍度信息.
  2.這些活躍度信息標識出那些regions最適合在STW期間回收(which regions will be best to reclaim during an evacuation pause).
  3.不像CMS有清理階段.
再次標記階段
  1.使用Snapshot-at-the-Beginning(SATB)算法比CMS快得多.
  2.空region直接被回收.
拷貝/清理階段(Copying/Cleanup Phase)
  1.年輕代與老年代同時回收.
  2.老年代內存回收會基於他的活躍度信息.


免責聲明!

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



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