1.1 引用計數法
?
每個對象創建的時候,會分配一個引用計數器,當這個對象被引用的時候計數器就加1,當不被引用或者引用失效的時候計數器就會減1。任何時候,對象的引用計數器值為0就說明這個對象不被使用了,就認為是“垃圾”,可以被GC處理掉。
?
【優點】算法實現簡單。
【缺點】不能解決對象之間循環引用的問題。有垃圾對象不能被正確識別,這對垃圾回收來說是很致命的,所以GC并沒有使用這種搜索算法。
1.2 根搜索算法
以一些特定的對象作為基礎原始對象,或者稱作“根”,不斷往下搜索,到達某一個對象的路徑稱為引用鏈。
如果一個對象和根對象之間有引用鏈,即根對象到這個對象是可到達的,則這個對象是活著的,不是垃圾,還不能回收。例如,假設有根對象O,O引用了A對象,同時A對象引用了B對象,B對象又引用了C對象,那么對象C和根對象O之間的路徑的可達的,C對象就不能當做垃圾對象。引用鏈為O->A->B->C。
反之,如果一個對象和根對象之間沒有引用鏈,根對象到這個對象的路徑是不可達的,那么這個對象就是可回收的垃圾對象。
?
?
【優點】可找到所以得垃圾對象,并且完美解決對象之間循環引用的問題。
【缺點】不可避免地要遍歷全局所有對象,導致搜索效率不高。
根搜索算法是現在GC使用的搜索算法。
?
可以當做GC roots的對象有以下幾種:
?
虛擬機棧中的引用的對象。(java棧的棧幀本地變量表)
?
方法區中的類靜態屬性引用的對象。
?
方法區中的常量引用的對象。(聲明為final的常量對象)
?
本地方法棧中JNI的引用的對象。(本地方法棧的棧幀本地變量表)
?
下面是從網上找來的圖,將就看看:GC ROOTS就是跟對象節點,藍色的是可達的引用鏈,引用鏈上的對象是活著的,不能被當做垃圾對象回收。相反暗灰色的路徑表示不可達的路徑,這些對象將會被回收。每個圈圈里面的數字,表示其被引用的次數,沒錯,就是上面說到的引用計數法的計數值。跟搜索算法示例圖
?
2.GC算法
這里討論的是oracle的Hotspot VM常見的垃圾回收算法。使用的搜索算法都是基于根搜索算法實現的。
?
2.1 標記-清除算法(Mark-Sweep)
該算法分兩步執行:
?
1) 標記Mark:從GC ROOTS開始,遍歷堆內存區域的所有根對象,對在引用鏈上的對象都進行標記。這樣下來,如果是存活的對象就會被做了標記,反之如果是垃圾對象,則沒做有標記。GC很容易根據有沒有被做標記就完成了垃圾對象回收。
?
2) 清除Sweep:遍歷堆中的所有的對象(標記階段遍歷的是所有根節點),找到未被標記的對象,直接回收所占的內存,釋放空間。
?
評價:
?
【優點】沒有產生額外的內存空間消耗,內存利用率高。
【缺點】效率低,清除階段要遍歷所有的對象;回收的垃圾對象是在各個角落的,直接回收垃圾對象,導致存在不連續的內存空間,產生內存碎片。
標記-清除算法操作的對象是【垃圾對象】,對于活著的對象(被標記的對象),它則直接不理睬。
?
2.2 復制算法(Copying)
復制算法把內存區間一分為二,有對象存在的一半區間稱為“活動區間”,沒有對象存在處于空閑狀態的空間則為“空閑區間”。
當內存空間不足時觸發GC,先采用根搜索算法標記對象,然后把活著的對象全部復制到另一半空閑區間上,復制算法的“復制”就來自這一操作。復制到另一半區間的時候,嚴格按照內存地址依次排列要存放的對象,然后一次性回收垃圾對象。
這樣原來的空閑區間在GC后就變成活動區間,而且內存順序齊整美觀。原來的活動區間在GC后就變成了完全空的空閑區間,等待下一次GC把活的對象被copy進來。
?
評價:
?
【優點】GC后的內存齊整,不產生內存碎片。
【缺點】GC要使用兩倍的內存,或者說導致堆只能使用被分配到的內存的一半,這個算法對空間要求太高!如果存活的對象較多,則意味著要復制很多對象并且要維護大量對象的內存地址,所以存活的對象數量不能太多,否則效率也會很低。
復制算法復制移動的對象是【活著的對象】,對于垃圾對象(不被標記的對象)則直接回收。
?
2.3 標記-整理算法(Mark-Compact)
這個算法則是對上面兩個算法的綜合結果。也分為兩個階段:
?
1)標記:這個階段和標記-清除Mark-Sweep算法一樣,遍歷GC ROOTS并標記存活的對象。
?
2)整理:移動所有活著的對象到內存區域的一側(具體在哪一側則由GC實現),嚴格按照內存地址次序依次排列活著的對象,然后將最后一個活著的對象地址以后的空間全部回收。
?
評價:
?
【優點】內存空間利用率高,消除了復制算法內存減半的情況;GC后不會產生內存碎片。
?
【缺點】需要遍歷標記活著的對象,效率較低;復制移動對象后,還要維護這些活著對象的引用地址列表。
?
2.4 分代回收算法(Generational Collecting)
分代回收算法就是現在JVM使用的GC回收算法。
?
2.4.1簡要說明
1)先來看看簡單化后的堆的內存結構:
?
Java堆 = 年老代 + 年輕代
(空間大小比例一般是3:1)
?
年輕代 = Eden區 + From Space區 + To Space區
(空間大小比例一般是8:1:1)
2)按照對象存活時間長短,我們可以把對象簡單分為三類:
?
短命對象:存活時間較短的對象,如中間變量對象、臨時對象、循環體創建的對象等。這也是產生最多數量的對象,GC回收的關注重點。
?
長命對象:存活時間較長的對象,如單例模式產生的單例對象、數據庫連接對象、緩存對象等。
?
長生對象:一旦創建則一直存活,幾乎不死的對象。
?
3)對象分配區域
短命對象存在于年輕代,長命對象存在于年老代,而長生對象則存在于方法區中。
由于GC的主要內存區域是堆,所以GC的對象主要就是短命對象和長命對象這類壽命“有限”的對象。
?
2.4.2 分代回收的GC類型
針對HotSpot VM的的GC其實準確分類只有兩大種:
?
1)Partial GC:部分回收模式
?
Young GC:只收集young gen的GC。和Minor GC一樣。
Old GC:只收集old gen的GC。只有CMS的concurrent - collection是這個模式
Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式
2)Full GC:收集整個堆,包括young gen、old gen,還有永久代perm gen(如果存在的話)等所有部分的模式。同Major GC。
?
3)觸發時機
HotSpot VM的串行GC的觸發條件是:
young GC:當young gen中的eden區分配滿的時候觸發。
?
full GC:當準備要觸發一次young GC時,如果發現統計數據說之前young GC的平均晉升大小比目前old gen剩余的空間大,則不會觸發young GC而是轉為觸發full GC;或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,默認也是觸發full GC。
?
并發GC的觸發條件就不太一樣。以CMS GC為例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做并發收集。
?
2.4.3 年輕代GC過程
當需要在堆中創建一個新的對象,而年輕代內存不足時觸發一次GC,在年輕代觸發的GC稱為普通GC,Minor GC。注意到年輕代中的對象都是存活時間較短的對象,所以適合使用復制算法。這里肯定不會使用兩倍的內存來實現復制算法了,牛人們是這樣解決的,把年輕代內存組成是80%的Eden、10%的From Space和10%的To Space,然后在這些內存區域直接進行復制。
?
剛開始創建的對象是在Eden中,此時Eden中有對象,而兩個survivor區沒有對象,都是空閑區間。第一次Minor GC后,存活的對象被放到其中一個survivor,Eden中的內存空間直接被回收。在下一次GC到來時,Eden和一個survivor中又創建滿了對象,這個時候GC清除的就是Eden和這個放滿對象的survivor組成的大區域(占90%),Minor GC使用復制算法把活的對象復制到另一個空閑的survivor區間,然后直接回收之前90%的內存。周而復始。始終會有一個10%空閑的survivor區間,作為下一次Minor GC存放對象的準備空間。
?
要完成上面的算法,每次Minor GC過程都要滿足:
存活的對象大小都不能超過survivor那10%的內存空間,不然就沒有空間復制剩下的對象了。但是,萬一超過了呢?前面我們提到過年老代,對,就是把這些大對象放到年老代。