Java垃圾回收

AngelDevil發表於2014-06-23

垃圾收集演算法

引用計數

堆中的每個物件都有一個引用計數,當物件被引用時引用計數加1,當物件的引用被重新賦值或超出有效區域時引用計數減1,當一個物件被回收後,它所引用的物件的引用計算減1。當一個物件的引用計數變為0時就被回收。

引用計數的優點:

垃圾收集器可以很快地執行,當一個物件的引用數為0時就可以回收這個物件,垃圾收集交織在程式的正常執行過程中,不用長時間中斷程式的正常執行。

引用計數的缺點:

  1. 每次引用計數的增加和減少會帶來額外的開銷
  2. 無法檢測出迴圈引用

根搜尋演算法

垃圾檢測通過建立一個根物件的集合(區域性變數、棧楨中的運算元,在本地方法中引用的物件,常量池等)並檢查從這些根物件開始的可觸及性來實現。根物件總是可訪問的,如果存在根物件到一個物件的引用路徑,那麼稱這個物件是可觸及的或活動物件,否則是不可觸及的,不可觸及的物件就是垃圾物件。

標記清除

分為標記和清除兩個階段,在標記階段,垃圾收集器跟蹤從根物件的引用,在追蹤的過程中對遇到的物件打一個標記,最終未被標記的物件就是垃圾物件,在清除階段,回收垃圾物件佔用的記憶體。可以在物件本身新增跟蹤標記,也可以用一個獨立的點陣圖來設定標記。

標記清除法是基礎的收集演算法,其他演算法大多時針對這個演算法缺點的改進。

有兩個缺點:

  1. 效率
  2. 存在記憶體碎片

複製演算法

將記憶體劃分為大小相等的兩個區域,每次只使用其中的一個區域,當這個區域的記憶體用完了,就將可觸及的物件直接複製到新的區域並連續存放以消除記憶體碎片,當可觸及物件複製完後,清除舊記憶體區域,修改引用的值。

這種演算法的缺點很明顯,可使用記憶體變為了原來的一半,太過浪費。

一般情況下,新生代中的物件大多生命週期很短,也就是說當進行垃圾收集時,大部分物件都是垃圾,只有一小部分物件會存活下來,所以只要保留一小部分記憶體儲存存活下來的物件就行了,用不著使用一半的記憶體。在新生代中一般將記憶體劃分為三個部分:一個較大的Eden空間和兩個較小的Survior空間(一樣大小),每次使用Eden和一個Survior的記憶體,進行垃圾收集時將Eden和使用的Survior中的存活的物件複製到另一個Survior空間中,然後清除這兩個空間的記憶體,下次使用Eden和另一個Survior,HotSpot中預設將這三個空間的比例劃分為8:1:1,這樣被浪費掉的空間就只有總記憶體的1/10了。

這樣的記憶體空間劃分是基於這樣一種假設,即每次垃圾收集時大部分物件都是垃圾,只有少部分物件存活。如果遇到例外的情況怎麼辦,在某次垃圾收集時存活下來的物件超過了預留的那個Survior空間的總大小,這就需要依賴其他的記憶體進行分配擔保了(參考分代收集,前面的描述中也說了這是新生代中的方法)

標記整理

普通的標記清除會在記憶體中留下記憶體碎片,複製演算法如果不想浪費掉50%記憶體就需要有記憶體分配擔保,一般是記憶體分代,但總有一代是沒有其他代為它擔保的。標記整理演算法中標記的過程同標記清理一樣,但整理部分不是直接清除掉垃圾物件,而是將活動物件統一移動一記憶體的一端,然後清除邊界外的記憶體區域,這樣就避免了記憶體碎片。也不會浪費記憶體,不需要其他記憶體進行擔保

分代收集

大多數程式中建立的大部分物件生命週期都很短,而且會有一小部分生命週期長的物件,為了克服複製收集器中每次垃圾收集都要拷貝所有的活動物件的缺點,將記憶體劃分為不同的區域,更多地收集短生命週期所在的記憶體區域,當物件經歷一定次數的垃圾收集存活時,提升它的存在的區域。一般是劃分為新生代和老年代。新生代又劃分為Eden區,From Survior區和To Survior區。

自適應收集器

監聽堆中的情形,並且對應地呼叫合適的垃圾收集技術。

垃圾收集器

Serial

一個單執行緒的收集器,在進行垃圾收集時會暫停其他執行緒的工作,不適合用到Server端的虛擬機器,但Client模式的模擬機還是可以用的,因為Client模式下的應用分配到的系統記憶體一般不大,垃圾收集可以很快完成。優點就是簡單高效,沒有執行緒互動開銷,可以獲得最高的單執行緒收集效率。

ParNew

Seria的多執行緒版本,可以多個執行緒收集垃圾,但如果CPU只有一核且沒有超執行緒,效果就不一定比Serial好了,如果是多核或有超執行緒,可以保證效果好於Serial,除Seria之外,這是唯一能與CMS收集器配合的垃圾收集器

Parallel Scavenge

使用複製演算法的新生代多執行緒垃圾收集器,Parallel Scavenge收集器的關注點和其他收集器不同,其他收集器的關注點是儘可能縮短垃圾收集時使用者執行緒等待的時間,而Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(Throughput),即CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值。以縮短使用者執行緒等待時間的收集器適合用於需要與使用者互動的程式,而以吞吐量為目標的收集器適合用於不需要和使用者太多的互動,以後臺運算為目標的任務。

Parallel Scavenge可以通過引數設定每次垃圾收集需要停頓的時間和吞吐量目標,但停頓時間並不是越小越好,這是以犧牲吞吐量和新生代空間為代價的,因為要使垃圾收集停頓時間縮小,只能進行少量多次收集,或減小需要收集的空間大小。

還有一個-XX:UseAdaptiveSizePolicy引數,指定這個引數後,就不需要手工指定新生代的大小、Eden區和Survior區的比例大小和晉升老年代物件年齡等細節引數了,虛擬機器會根據收集到的資訊動態調整這些引數,這稱為自適應策略。

Serial Old

Serial的老年代版本,單執行緒收集器,使用"標記-整理"演算法,主要被Client模式下的虛擬機器使用,當被使用在Server模式時主要有兩個用途:

  1. 與Parallel Scavenge配合使用
  2. 作為CMS收集失敗時的備選方案。

Parallel Old

Parallel Scavenge的老年代版本,使用"標記-整理"演算法,JDK1.6後提供的,在此之前,如果新生代選擇了Parallel Scavenge,老年代只能選擇Serial Old,由於Serial Old是單執行緒的垃圾收集器,可能會影響收集效能。Parallel Old出現後,就可以分別在新生代和老年代選擇Parallel Scavenge和Parallel Old組合了。

CMS(Concurrent Mark Sweep)

以獲取最短回收停頓時間為目標的收集器,使用“標記-清除”演算法,整個回收過程分為以下4步:

  • 初始標記(CMS Initial Mark)
  • 併發標記(CMS Current Mark)
  • 重新標記(CMS Remark)
  • 併發清楚(CMS Concurrent Sweep)

初始標記與重新標記階段仍會暫停使用者執行緒的執行。

初始標記只是記錄下GC Root能直接關聯到的物件,速度很快。

併發標記就是GC Roots Tracing了,速度較慢,但可以和使用者執行緒同時執行。

重新標記是修正併發標記時由於使用者執行緒執行導致的標記記錄變動,這個階段會使使用者執行緒停頓,停頓時間比初始標記略長,但仍小於重新標記。

併發清除就是清除垃圾物件了,耗時較長,但可與使用者執行緒同時工作。

CMS的缺點

  1. 對CPU資源敏感,併發階段和使用者執行緒同時執行,影響伺服器的響應速度,尤其是CPU核心數少時
  2. 無法處理浮動垃圾,由於併發階段使用者執行緒同時在執行,可能會在垃圾收集過程中產生新的垃圾,CMS無法處理這部分浮動垃圾,由於在進行垃圾收集時使用者執行緒同時在執行,需要額外的記憶體空間,所以不能等到記憶體滿時再進行GC,需要預留一部分空間,如果預留的這部分空間不夠GC時使用者執行緒建立新物件使用,就會使用預備方法,使用Serial Old進行一次Full GC。
  3. CMS基於“標記-清除”演算法,進行垃圾回收後會存在記憶體碎片,當申請大的連續記憶體時可能記憶體不足,此時需要進行一次Full GC,可以通過引數指定進行Full GC後或進行多少次Full GC後進行一次記憶體壓縮來整理記憶體碎片。

G1(Garbage First)

基於"標記-整理"演算法,避免了記憶體碎片的問題,並可精確地控制垃圾回收時的停頓。

G1收集器可以實現基本不犧牲吞吐量的前提下完成低停頓的記憶體回收,不同於之前的垃圾回收器,G1收集器的回收區域不是整個新生代或老年代,而是將整個Java堆劃分為多個固定大小的區域,並跟蹤這些區域裡的垃圾堆積程度,在後臺維護一個優先列表,優先回收垃圾最多的區域。區域的劃分使每次回收時間變短,而優先順序的劃分使得每次回收的區域可以回收最多的垃圾,這就使用G1收集器可以在有限的時間內獲取最高的收集效率。

記憶體分配與回收策略

對像優先在新生代Eden區分配,當Eden區沒有足夠的記憶體時會發生一次Minor GC(新生代GC,Major GC或Full GC是老年代GC)

大物件可以直接在老年代分配記憶體,可以通過引數指定一個大小,大於這個大小的物件直接在老年代中分配記憶體。

進行Minor GC時,Eden區和一個Survior區中存活的物件會被複制到另一個Survior區,一個物件每在一次Minor GC中存活下來一次後這個物件的年齡就加1,當這個物件的年齡大於一定值(預設15)就會進入老年代。

如果Survior中相同年齡的物件佔用的空間大於Survior空間的一半,那麼年齡大於或等於這個年齡的物件會直接進入老年代,而不用等到達到特定年齡

當進行Minor GC時,虛擬機器會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果小於,判斷是否開啟了HandlerPromotionFailure允許擔保失敗,如果開啟了就只進行Minor GC,否則進行Full GC。由於使用的之前Minor GC時的平均大小,如果某一次突然大小變大,導致老年代剩餘空間不夠,即擔保失敗,會再進行一次Full GC。

finalize

GC時會對活動物件進行標記,沒有被標記的物件就是垃圾物件,但垃圾物件不會直接被清除,垃圾收集器還會判斷是否需要執行物件的finalize方法,如果物件沒有覆寫finalize方法或它的finalize已經被執行過一次,那麼是沒有必要執行的,否則就認為是有必要執行的,當被判斷為有必要執行時,這個物件會被放入一個F-Queue佇列中,由一個後臺的低優先順序的Finalizer執行緒執行佇列中的物件的finalize方法,物件可以在這個方法中中復活自己,即重新被其他物件引用,但這個函式只會被垃圾收集器執行一下,第二次回收這個物件時這個函式不會再被呼叫。稍後GC會對F-Queue佇列中的物件執行第二次標記。

相關文章