天天看點

垃圾收集器與記憶體配置設定政策判斷對象死亡常用方法垃圾收集算法垃圾收集器記憶體配置設定與回收政策

經過半個多世紀的發展、目前記憶體的動态配置設定與記憶體回收技術已經相當成熟,但作為程式猿還是得了解GC和記憶體配置設定。當需要排查各種記憶體溢出、記憶體洩漏、當垃圾收內建為系統達到更高并發量的瓶頸時,就需要對記憶體的動态配置設定與記憶體回收技術實施必要的監控和調節。

  本文講叙了記憶體中垃圾的收集及記憶體配置設定政策。相比較而言,垃圾收集更難一些。本文将介紹幾種常見的垃圾收集器及常用垃圾收集算法。垃圾收集算法是基于判斷對象在記憶體中是否死亡,隻有判斷确定出對象已經死亡,才能采取不同的方式進行收集,實作記憶體的回收。

引用計數算法(Reference Counting):給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就+1;當引用失效時,計數器值就-1;任何時刻計數器為0的對象就是不可能再被使用的。實作簡單、判定效率高。很難解決對象之間的循環引用問題,目前主流Java虛拟機裡沒有選用計數算法來管理記憶體。

可達性分析算法(Reachability Analysis):通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可引用的。即使是不可達的對象,也并非是“非死不可”的,隻是暫時處于“緩刑”階段,真正宣告死亡,至少還是經曆再次标記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接配接的引用鍊,則将會被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法,或者finalize()方法已經被虛拟機調用過,虛拟機将這兩種情況都視為“沒有必要執行”。在Java語言中,可作為GC Roots的對象有四種:(1)虛拟機棧中引用的對象;(2)方法區中類靜态屬性引用的對象;(3)方法區中常量引用的對象;(4)本地方法棧中JNI引用的對象。

  判斷對象是否存活與“引用”相關。引用可分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種,其引用強度依次減弱。

标記—清除算法(Mark-Sweep):分為“标記”和“清除”兩個階段,首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。主要兩處不足:一個是效率問題,标記和清除兩個過程的效率都不高;二是空間問題,标記清除之後會産生大量不連續的記憶體碎片。

複制算法(Copying):将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中一塊,當這一塊用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次性清理掉。可根據實際需求将兩塊記憶體按不同比例劃分。現在商業虛拟機都采用這種收集算法來回收新生代。在對象存活率較高時,就要進行較多的複制操作,效率會變低。

标記—整理算法(Mark-Compact):标記的過程與“标記—清除”算法一樣,隻是在整理階段,讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的的記憶體。

分代收集算法(Generational Collection):根據對象存活周期的不同将記憶體劃分為幾塊,一般将Java堆分為新生代和老年代,再根據各個年代的特點采用最适當的收集算法。

垃圾收集器是記憶體回收的具體實作。基于JDK 1.7 Update 14之後的HotSpot虛拟機所包含的收集器如下圖所示:

垃圾收集器與記憶體配置設定政策判斷對象死亡常用方法垃圾收集算法垃圾收集器記憶體配置設定與回收政策

Serial收集器:單線程收集器,隻使用一個CPU或一條線程去完成垃圾收集工作,在垃圾收集時,必須暫停其他所有工作線程,直接收集結束。對于運作在Client模式下的虛拟機來說是一個很好的選擇。

ParNew收集器:Serial收集器的多線程版本。運作在Server模式下的虛拟機中首先的新生代收集器。除了Serial收集器外,目前隻有ParNew收集器能與CMS收集器配合工作。

Parallel Scavenge收集器:也稱為“吞吐量優先”收集器,關注點是達到一個可控制的吞吐量(Throughput)。吞吐量=運作使用者代碼時間/(運作代碼時間+垃圾收集時間)。自适應調節政策也是Parallel Scavenge收集器與ParNew收集器的一個重要差別。GC自适應調節政策(GC Ergonomics)是指虛拟機根據目前系統的運作情況收集性能監控資訊,動态調整參數-XX:+UseAdaptiveSizePolicy以提供最合适的停頓時間或者最大的吞吐量。

Serial Old 收集器:單線程收集器,使用“标記—整理”算法。主要意義是在于給Client模式下的虛拟機使用。

Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多線程和“标記—整理”算法,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

CMS收集器(Concurrent Mark Sweep):以擷取最短回收停頓時間為目标的收集器。整個過程可分為初始标記(CMS initial mark)、并發标記(CMS concurrent mark)、重新标記(CMS remark)、并發清除(CMS concurrent sweep)4個步驟。整個過程中耗時最長的是并發标記和并發清除過程,但它們都可以與使用者線程一起工作。總體來說,CMS收集器的記憶體回收過程是與使用者線程一起并發執行的。有3個明顯的缺點:(1)CMS收集器對CPU資源非常敏感;(2)CMS收集器無法處理浮動垃圾(Floating Garbage);(3)收集結束時會有大量的空間碎片産生。

G1收集器(Garbage-First):當今收集器技術發展最前沿成果之一。在G1之前的收集器收集範圍是整個新生代或者老年代,G1不再是這樣。G1将整個堆劃分為多個大小相等的獨立區域(Region),G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region,進而保證了G1收集器在有限的時間内可以擷取盡可能高的收集效率。運作分為4個步驟:初始标記(Initial Marking)、并發标記(Concurrent Marking)、最終标記(Final Marking)和篩選回收(Live Data Counting and Evacuation)。具備如下特點:并行與并發、分代收集、空間整合、可預測的停頓。

對象優先在Eden配置設定:大多數情況下,對象在新生代Eden區中配置設定。當Eden區沒有足夠的空間進行配置設定時,虛拟機将發起一次Minor GC。

大對象直接進行老年代:所謂大對象是指需要大量連續記憶體空間的Java對象,很長的字元串以及數組就是最典型的大對象。

長期存活的對象将進入老年代:虛拟機給每個對象定義一個對象年齡(Age)計數器。如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當年齡達到一定程度時,就會被晉升到老年代中。晉升門檻值可以通過參數設定。

動态對象年齡判定:為了能更好地适應不同的記憶體狀況,虛拟機并不是永遠地要求對象的的年齡必須達到MaxTenuringThreshold才能晉升老年代。如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可直接進入老年代。

空間配置設定擔保:在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,以確定Minor GC是安全的。如果不成立,則檢視HandlePromotionFailure設定值是否允許擔保失敗。若允許,則會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試進行一次Minor GC,但有一定的風險;如果小于,或者HandlePromotionFailure設定不允許冒險,則改為進行一次Full GC。