天天看點

JVM垃圾回收機制

1.Hotspot 架構

HotSpot JVM 有一個穩定強悍的架構, 支援強大的功能與特性, 具備實作高性能和大規模可伸縮性的能力。例如,HotSpot JVM JIT編譯器能動态進行優化生成。換句話說,他們運作Java程式時,會針對底層系統架構動态生成高性能的本地機器指令。此外,通過成熟的演進和運作時環境的持續工程,加上多線程垃圾收集器,HotSpot JVM即使實在大型計算機系統上也能獲得很高的伸縮性.

JVM垃圾回收機制

2.  何為GC

垃圾回收機制是由垃圾收集器Garbage Collection來實作的,GC是背景一個低優先級的守護程序。在記憶體中低到一定限度時才會自動運作,是以垃圾回收的時間是不确定的。

3. 何為垃圾

Java中那些不可達的對象就會變成垃圾。對象之間的引用可以抽象成樹形結構,通過樹根(GC Roots)作為起點,從這些樹根往下搜尋,搜尋走過的鍊稱為引用鍊。當一個對象到GC Roots沒有任何引用鍊相連時,則證明這個對象為可回收的對象。可以作為GC Roots的主要有以下幾種:

(1)棧幀中的本地變量表所引用的對象。

(2)方法區中類靜态屬性和常量引用的對象。

(3)本地方法棧中JNI(Native方法)引用的對象。

 //垃圾産生的情況舉例:

//1.改變對象的引用,如置為null或者指向其他對象

Object obj1 = new Object();

Object obj2 = new Object();

obj1 = obj2; //obj1成為垃圾

obj1 = obj2 = null ; //obj2成為垃圾

4.  典型的垃圾回收算法

在确定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裡面涉及到一個問題是:如何高效地進行垃圾回收。下面讨論幾種常見的垃圾收集算法。

4.1     Mark-Sweep(标記-清除)算法

标記-清除算法分為兩個階段:标記階段和清除階段。标記階段的任務是标記出所有需要被回收的對象,清除階段就是回收被标記的對象所占用的空間。标記-清除算法實作起來比較容易,但是有一個比較嚴重的問題就是容易産生記憶體碎片,碎片太多可能會導緻後續過程中需要為大對象配置設定空間時無法找到足夠的空間而提前觸發GC。

4.2    Copying(複制)算法

Copying算法将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把第一塊記憶體上的空間一次清理掉,這樣就不容易出現記憶體碎片的問題,并且運作高效。但是該算法導緻能夠使用的記憶體縮減到原來的一半。而且,該算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麼Copying算法的效率将會大大降低。(這也是為什麼後面提到的新生代采用Copying算法)

4.2    Mark-Compact(标記-整理)算法

為了解決Copying算法的缺陷,充分利用記憶體空間,提出了Mark-Compact算法。該算法标記階段标記出所有需要被回收的對象,但是在完成标記之後不是直接清理可回收對象,而是将存活的對象都移向一端,然後清理掉端邊界以外的所有記憶體(隻留下存活對象)。

4.4    Generational Collection(分代收集)算法

分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是将堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時隻有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以在不同代的采取不同的最适合的收集算法。

目前大部分垃圾收集器對于新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要複制的操作次數較少,該算法效率在新生代也較高。但是實際中并不是按照1:1的比例來劃分新生代的空間的,一般來說是将新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間(比例8:1:1),每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,将還存活的對象複制到另一塊Survivor空間中,然後清理掉Eden和A空間。在進行了第一次GC之後,使用的便是Eden space和B空間了,下次GC時會将存活對象複制到A空間,如此反複循環。

當對象在Survivor區躲過一次GC的話,其對象年齡便會加1,預設情況下,對象年齡達到15時,就會移動到老年代中。一般來說,大對象會被直接配置設定到老年代,所謂的大對象是指需要大量連續存儲空間的對象,最常見的一種大對象就是大數組,比如:byte[] data = newbyte[4*1024*1024]。

當然配置設定的規則并不是百分之百固定的,這要取決于目前使用的是哪種垃圾收集器組合和JVM的相關參數。這些搬運工作都是GC完成的,GC不僅負責在Heap中搬運執行個體,同時負責回收存儲空間。

最後,因為每次回收都隻回收少量對象,是以老年代一般使用的是标記整理算法。

注意,在方法區中有一個永久代(Permanet Generation),它用來存儲class檔案、靜态對象、方法描述等。對永久代的回收主要回收兩部分内容:廢棄常量和無用的類。

Minor GC是新生代Copying算法。MinorGC觸發條件:

(1)當Eden區滿時,觸發Minor GC。      

Full GC的老年代,采取的Mark-Compact。Full GC觸發條件:

(1)調用System.gc時,系統建議執行Full GC,但是不必然執行。

(2)老年代空間不足。

(3)方法區空間不足。

(4)通過Minor GC後進入老年代的平均大小大于老年代的可用記憶體。

有關檢視垃圾回收資訊的JVM常見配置方式:

-XX:+PrintGCDetails

最後介紹一下有關堆的JVM常見配置方式:

-Xss //選置棧記憶體的大小

-Xms: //初始堆大小

-Xmx: //最大堆大小

-XX:NewSize=n: //設定年輕代大小

-XX:NewRatio=n: //設定年輕代和年老代的比值。比如設定為3,表示年輕代與年老代比值為1:3

-XX:SurvivorRatio=n: //年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。比如設定為3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5。

-XX:MaxPermSize=n: //設定持久代大小

5.  典型的垃圾回收器

1.Serial收集器

Serial收集器是最基本、發展曆史最悠久的收集器。是單線程的收集器。它在進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集完成。

JVM垃圾回收機制

Serial收集器依然是虛拟機運作在Client模式下預設新生代收集器,對于運作在Client模式下的虛拟機來說是一個很好的選擇。

2.ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制參數、收集算法、Stop The Worl、對象配置設定規則、回收政策等都與Serial 收集器完全一樣。

JVM垃圾回收機制

ParNew收集器是許多運作在Server模式下的虛拟機中首選新生代收集器,其中有一個與性能無關但很重要的原因是,除Serial收集器之外,目前隻有ParNew它能與CMS收集器配合工作。

3.Parallel Scavenge(并行回收)收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複制算法的收集器,又是并行的多線程收集器

該收集器的目标是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運作使用者代碼的時間與CPU總消耗時間的比值,即 吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)停頓時間越短就越适合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可用高效率地利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。

Parallel Scavenge收集器提供兩個參數用于精确控制吞吐量,分别是控制最大垃圾收起停頓時間的

-XX:MaxGCPauseMillis參數以及直接設定吞吐量大小的-XX:GCTimeRatio參數

Parallel Scavenge收集器還有一個參數:-XX:+UseAdaptiveSizePolicy。這是一個開關參數,當這個參數打開後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數,隻需要把基本的記憶體資料設定好(如-Xmx設定最大堆),然後使用MaxGVPauseMillis參數或GCTimeRation參數給虛拟機設立一個優化目标。自适應調節政策也是Parallel Scavenge收集器與ParNew收集器的一個重要差別

4.Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用标記整理算法。這個收集器的主要意義也是在于給Client模式下的虛拟機使用。

如果在Server模式下,主要兩大用途:

(1)在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用

(2)作為CMS收集器的後備預案,在并發收集發生Concurrent Mode Failure時使用

Serial Old收集器的工作工程

5.Parallel Old 收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多線程和“标記-整理”算法。這個收集器在1.6中才開始提供。

6.CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視伺服器的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求

CMS收集器是基于“标記-清除”算法實作的。它的運作過程相對前面幾種收集器來說更複雜一些,整個過程分為4個步驟:

(1)初始标記

(2)并發标記

(3)重新标記

(4)并發清除

其中,初始标記、重新标記這兩個步驟仍然需要“Stop The World”.

JVM垃圾回收機制

CMS收集器主要優點:并發收集,低停頓。

CMS三個明顯的缺點:

(1)CMS收集器對CPU資源非常敏感。CPU個數少于4個時,CMS對于使用者程式的影響就可能變得很大,為了應付這種情況,虛拟機提供了一種稱為“增量式并發收集器”的CMS收集器變種。所做的事情和單CPU年代PC機作業系統使用搶占式來模拟多任務機制的思想

(2)CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導緻另一次Full GC的産生。在JDK1.5的預設設定下,CMS收集器當老年代使用了68%的空間後就會被激活,這是一個偏保守的設定,如果在應用中藍年代增長不是太快,可以适當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低記憶體回收次數進而擷取更好的性能,在JDK1.6中,CMS收集器的啟動閥值已經提升至92%。

(3)CMS是基于“标記-清除”算法實作的收集器,收集結束時會有大量空間碎片産生。空間碎片過多,可能會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來配置設定目前對象,不得不提前出發FullGC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關參數(預設就是開啟的),用于在CMS收集器頂不住要進行FullGC時開啟記憶體碎片合并整理過程,記憶體整理的過程是無法并發的,空間碎片問題沒有了,但停頓時間變長了。虛拟機設計者還提供了另外一個參數-XX:CMSFullGCsBeforeCompaction,這個參數是用于設定執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(預設值為0,辨別每次進入Full GC時都進行碎片整理)

7. G1收集器

G1收集器的優勢:

(1)并行與并發

(2)分代收集

(3)空間整理 (标記整理算法,複制算法)

(4)可預測的停頓(G1處處理追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經實作Java(RTSJ)的來及收集器的特征)

使用G1收集器時,Java堆的記憶體布局是整個規劃為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分Region的集合。

G1收集器之是以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在真個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所擷取的空間大小以及回收所需要的時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的又來)。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間内可以擷取盡量可能高的灰機效率

G1 記憶體“化整為零”的思路

在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆掃描也不會遺漏。

如果不計算維護Remembered Set的操作,G1收集器的運作大緻可劃分為一下步驟:

(3)最終标記

(4)篩選回收

JVM垃圾回收機制
最後介紹一下有關收集器設定的JVM常見配置方式: