1. 程式人生 > >JVM(六)垃圾回收機制---垃圾回收演算法和垃圾分類器種類

JVM(六)垃圾回收機制---垃圾回收演算法和垃圾分類器種類

針對HotSpot VM的實現,它裡面的GC其實準確分類只有兩大種:

  • Partial GC:並不收集整個GC堆的模式
  • Young GC:只收集young gen的GC
  • Old GC:只收集old gen的GC。只有CMS的concurrent collection是這個模式
  • Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
  • Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

最簡單的分代式GC策略,按HotSpot VM的serial GC的實現來看,觸發條件是:

  • young GC:當young gen中的eden區分配滿的時候觸發。注意young GC中有部分存活物件會晉升到old gen,所以young GC後old gen的佔用量通常會有所升高。
  • full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。

垃圾收集演算法 

  • 標記清除演算法

在上一篇講過了兩次標記的演算法,那麼標記清除的演算法,就是當GC把一個物件兩次標記,那麼下一次gc就會直接清除這些物件。但是標記和清除是分步的,首先比標記處所有要回收的物件,再統一回收好處就是效率快,兩次過程效率都很慢,也會造成記憶體不連續,那麼以後如果堆要被很大的物件分配記憶體可能會溢位

  • 標記整理

標記清除演算法的改進,在標記的過程是一樣的,只是後面處理不一樣,gc會把當前存活的物件移到一邊,然後從邊界端開始清除被兩次標記過的物件

  • 複製演算法

當前商業虛擬機器都採用這種方式收集新生代。

1:1:就是每次把可用的記憶體分成兩塊,當其中一塊不夠用時,就把這塊中活著的物件複製到另一塊中,然後清除另一塊剩下的物件。好處效率快,執行簡單,但是要犧牲掉一半記憶體為代價

1:1:8:為了解決上面的問題,複製演算法:我們都知道新生代物件一般都是朝生夕亡,所以不用為他分配那麼大的記憶體,而是把記憶體劃分較大的一塊,和較小的兩塊survivor,每次分配只是用一塊survivor和較大的那塊,然後要gc時,就把活著的物件移到survivor2中,清理比較大的那塊記憶體和survivor1剛才使用的空間。相當與新生代物件浪費10%的資源,但是萬一新生代物件比較大,就需要老年代記憶體空間來給擔保,碰到放不下的大新生物件,直接存入老年代,這種方法適合新生代物件,因為在向survivor2複製的時候,物件比較少

  • 分代收集演算法

目前jdk內部的gc一般採用分代收集演算法,它沒有創新點,只是針對不同代的物件採用不同的垃圾收集演算法,新生代複製演算法,老年代標記整理演算法。

垃圾回收器 

垃圾回收器就是垃圾回收的具體演算法。沒有最好的垃圾回收器,組合使用。

不同版本的虛擬機器提供的垃圾回收器都不一樣

新生代垃圾回收器

  • serial收集器

jdk1.3之前是唯一用來收集新生代物件的垃圾回收器,採用複製演算法,是單執行緒的垃圾回收器。這裡的單執行緒有兩個意義:一個就是該垃圾回收器只有一條執行緒來回收垃圾。還有一層意義是在它工作的時候,所有其他正常工作的執行緒都必須停止等待(stop the world),所以等待時間長短是它的限制。當然在使用者不可見的時候,停下你的執行緒 ,當然也可以理解,你媽幫你打掃衛生,你還一邊丟垃圾就是不行的

雖然這個垃圾回收器老,也不知道多CPU,但是限定單個CPU的環境,在使用者機器上,一般不會給虛擬機器分配很大的記憶體, ,收集的新生代也很少,那麼執行緒等待的時間不會很長,可以接受。所以在client模式下的虛擬機器,serial收集器是不錯的選擇

 是serial收集器的多執行緒版本。過程中採用的收集演算法是一樣的。只是它支援多執行緒來執行收集演算法。所以是server模式下的虛擬機器中垃圾回收器不錯的選擇,目前只有它和serial能與CMS收集器配合使用。單CPU效能不如serial收集器,多CPU下,預設啟動跟Cpu數量一樣的執行緒數,也可以用XX:ParallelGCThreads來設定啟動的執行緒數 

  • parnew收集器

  • parallel Scavenge收集器

又叫吞吐量優先的gc,目的是最大化cpu的吞吐量,不太關注gc停頓時間,適合在後臺不需要太多的互動。

cpu吞吐量=(執行使用者程式碼的時間)/(執行使用者程式碼的時間+執行垃圾回收的時間)

提供兩個引數,-XX:MaxGCPauseMillis引數,設定gc最大停頓時間

-XX:GCTimeRatio:直接設定最大吞吐量

這兩個引數是相互影響的,不能說把-XX:MaxGCPauseMillis設定的小一些,就能使垃圾回收速度變壞,這個時間縮短是以犧牲吞吐量和新生代空間來換去的。那麼每次執行gc的時間就太短,一定量的垃圾要分好幾次才能執行完,一定時間內的吞吐量就下降了

GC自適應的調節策略。最重要的引數,自動調優引數-XX:UserAdaptiveSizePolicy,預設開啟,就不用程式設計師指定其他引數,或者通過你自己更關注最大停頓時間還是吞吐量來設定虛擬機器的優化目標。

老年代垃圾回收器

  • serial old收集器

採用標記整理演算法回收老年代物件

作用:(1)jdk1.5之前與parallel Scavenge收集器搭配使用,但是由於單執行緒的限制使arallel得p Scavenge收集器體現不到價值

 (2)作為CMS收集器的備選,當CMS出現concurrent mode faillure錯誤時,虛擬機器會暫時呼叫serial old收集器

  • parallel old收集器

採用標記,整理演算法回收老年代物件

作用:(1)jdk1.6之後與parallel Scavenge收集器搭配使用,使得parallel Scavenge收集器體現價值

在注重吞吐量及cpu資源敏感可以選擇parallel Scavenge+parallel old收集器

  • CMS收集器 

採用改進的標記清除演算法收集老年代物件,目標是縮短gc停頓時間。一定意義上的併發回收

收集步驟:

(1)初始標記:需要暫停掉其他執行緒,然後僅僅標記GC roots能直接關聯到的物件,執行時間短

(2)併發標記:回收執行緒與使用者執行緒併發執行,進行gc roots tracing,執行時間較長

(3)重新標記:需要暫停掉其他執行緒,重新標記使用者執行緒在併發標記階段引用改變的物件,執行時間也比較短

(4)併發清除:併發收集被標記的物件,執行時間較長

可以看到在最耗時的兩個階段,回收執行緒都與使用者執行緒併發執行,所以總體上可以說CMS收集器是和使用者執行緒併發執行的

問題

(1)清理不到漂浮的物件,就是在併發清除階段,使用者執行緒還在執行就會產生新的垃圾,所以CMS在收集老年代物件時,不能等到老年代物件的記憶體滿了才開始回收,得留一部分空間給給執行的使用者執行緒,CMS提供一個引數-XX:CMSInittiatingOccupancyFaction來決定當老年代使用了百分之多少記憶體就觸發CMS回收器。在jdk1.6,這個引數預設是92%。是一個比較高的數,那麼當到達這個容量,收集開始,發現剩餘容量不夠併發的執行緒使用,就會出現concurrent mode faillure錯誤時,虛擬機器會暫時呼叫serial old收集器。這樣停頓時間就很長了。

(2)併發階段還是會佔用cpu資源,導致引用程式變慢,總吞吐量降低,CMS預設啟動的回收執行緒數是(cpu數+3)/4

當cpu數大於4時,回收執行緒佔cpu資源的25%以內,可以接受,但是當cpu數小於4,比如兩個cpu的情況,回收執行緒佔用50%的cpu資源的,你的程式速度減半,也會很明顯的。

(3)使用標記清除演算法,造成空間不連續問題。所以CMS還有兩個引數-XX:CMSCompactAtFullCollection,就是記憶體不夠分配必須進行full GC(新生代和老年代都回收,System.gc()觸發full gc)之前 CMS開始進行整理壓縮記憶體空間,這個引數預設是開的。

第二個引數:-XX:CMSFullGCsBeforeCompaction,就是設定CMS執行多少次full GC不壓縮後,來一次壓縮整理的。

全能垃圾收集器G1(Garbage first)

最先進的回收器,可以收集老年代新生代,是面向伺服器的垃圾回收器。

結構特點

在G1裡,雖然繼續有新生代和老年代的概念,但他們不在物理上隔離,不是分開儲存的,G1收集器,把記憶體劃分為等大的一些regins。

G1優點:

併發與並行:目的也是減少GC停頓時間

分代收集:採用分代收集演算法,不需要配合其他收集器

空間整合:整體來看是標記整理演算法,執行完不會有空間碎片(後面具體解釋),優於CMS

可預測的停頓:優於CMS.能指定在M毫秒的時間段裡,垃圾回收停頓最大不超過的毫秒數。

G1是regin層面上的操作,它會評估每個regin包含垃圾的回收價值,即把這些垃圾回收了能釋放多少記憶體,以及回收所需要的時間,那麼根據回收價值,G1維護一個優先佇列,先收集回收價值高的regin,這樣能提高整體效率。

所有回收器都有的問題(新老年代物件互相引用)

在JVM中回收垃圾的思想都是分代收集,只是G1是自己一個收集器完成,其他是需要配合完成,

前面我們說到當進行列舉根節點時使用oopmap記錄一些引用位置,這個概念是在新生代收集器或者老年代收集器中進行的

忽略的問題就是,新老年代之間可能存在互相引用,那麼不論是新生代收集器還是老年代收集器在做可達性分析,列舉根節點時,還不是要掃描整個堆?當然不會,虛擬機器這個時候使用Remembered Set來記錄新老年代之間的相互引用,當發生引用關係變化時或者加入新的引用關係時,先判斷是不是不同代之間的引用,是就把這個引用記錄到Remembered Set,這個Remembered Set是整個堆共享的,那麼不同年代的回收器在gc回收,進行各節點列舉時只要把Remembered Set加入,就能保證不掃描全隊又不會有遺漏。

所以在回收演算法,可達性分析時用到的資料結構有Remembered Set和oopmap

oopmap記錄的老年代或者新生代內部的引用,Remembered Set記錄新老年代之間的相互引用。

G1回收步驟

(1)初始標記

(2)併發標記

(3)最終標記

(4)篩選回收

1,2,3階段與CMS相同,4篩選回收就是根據各個regin的回收價值以及使用者希望的gc停頓時間來制定回收計劃