JVM垃圾回收總結

轉自:http://blog.csdn.net/dc_726/article/details/7934101

垃圾回收包含的內容不少,但順着下面的順序捋清知識也並不難。首先要
搞清垃圾回收的範圍(棧需要GC去回收嗎?),然後就是回收的前提條件
如何判斷一個對象已經可以被回收(這裏只重點學習根搜索算法就行了),
之後便是建立在根搜索基礎上的三種回收策略,最後便是JVM中對這三種
策略的具體實現。

1.範圍:要回收哪些區域?

Java方法棧、本地方法棧以及PC計數器隨方法或線程的結束而自然被回收,
所以這些區域不需要考慮回收問題。Java堆和方法區是GC回收的重點區域,
因爲一個接口的多個實現類需要的內存不一樣,一個方法的多個分支需要
的內存可能也不一樣,而這兩個區域又對立於棧可能隨時都會有對象不再
被引用,因此這部分內存的分配和回收都是動態的。


2.前提:如何判斷對象已死?

(1)引用計數法

引用計數法就是通過一個計數器記錄該對象被引用的次數,方法簡單高效,
但是解決不了循環引用的問題。比如對象A包含指向對象B的引用,對象B
也包含指向對象A的引用,但沒有引用指向A和B,這時當前回收如果採用的
是引用計數法,那麼對象A和B的被引用次數都爲1,都不會被回收。

下面是循環引用的例子,在Hotspot JVM下可以被正常回收,可以證實JVM
採用的不是簡單的引用計數法。通過-XX:+PrintGCDetails輸出GC日誌。
[java] view plaincopy
  1. package com.cdai.jvm.gc;  
  2.   
  3. public class ReferenceCount {  
  4.   
  5.     final static int MB = 1024 * 1024;  
  6.       
  7.     byte[] size = new byte[2 * MB];  
  8.       
  9.     Object ref;  
  10.       
  11.     public static void main(String[] args) {  
  12.         ReferenceCount objA = new ReferenceCount();  
  13.         ReferenceCount objB = new ReferenceCount();  
  14.         objA.ref = objB;  
  15.         objB.ref = objA;  
  16.           
  17.         objA = null;  
  18.         objB = null;  
  19.           
  20.         System.gc();  
  21.         System.gc();  
  22.     }  
  23.   
  24. }  

[Full GC (System) [Tenured: 2048K->366K(10944K), 0.0046272 secs] 4604K->366K(15872K), [Perm : 154K->154K(12288K)], 0.0046751 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 

(2)根搜索

通過選取一些根對象作爲起始點,開始向下搜索,如果一個對象到根對象
不可達時,則說明此對象已經沒有被引用,是可以被回收的。可以作爲根的
對象有:棧中變量引用的對象,類靜態屬性引用的對象,常量引用的對象等。
因爲每個線程都有一個棧,所以我們需要選取多個根對象。



附:對象復活

在根搜索中得到的不可達對象並不是立即就被標記成可回收的,而是先進行一次
標記放入F-Queue等待執行對象的finalize()方法,執行後GC將進行二次標記,復活
的對象之後將不會被回收。因此,使對象復活的唯一辦法就是重寫finalize()方法,
並使對象重新被引用。
[java] view plaincopy
  1. package com.cdai.jvm.gc;  
  2.   
  3. public class DeadToRebirth {  
  4.   
  5.     private static DeadToRebirth hook;   
  6.       
  7.     @Override  
  8.     public void finalize() throws Throwable {  
  9.         super.finalize();  
  10.         DeadToRebirth.hook = this;  
  11.     }  
  12.       
  13.     public static void main(String[] args) throws Exception {  
  14.         DeadToRebirth.hook = new DeadToRebirth();  
  15.         DeadToRebirth.hook = null;  
  16.         System.gc();  
  17.         Thread.sleep(500);  
  18.         if (DeadToRebirth.hook != null)  
  19.             System.out.println("Rebirth!");  
  20.         else  
  21.             System.out.println("Dead!");  
  22.           
  23.         DeadToRebirth.hook = null;  
  24.         System.gc();  
  25.         Thread.sleep(500);  
  26.         if (DeadToRebirth.hook != null)  
  27.             System.out.println("Rebirth!");  
  28.         else  
  29.             System.out.println("Dead!");  
  30.     }  
  31.       
  32. }  

要注意的兩點是:
第一,finalize()方法只會被執行一次,所以對象只有一次復活的機會。
第二,執行GC後,要停頓半秒等待優先級很低的finalize()執行完畢。


3.策略:垃圾回收的算法

(1)標記-清除

沒錯,這裏的標記指的就是之前我們介紹過的兩次標記過程。標記完成後就可以
對標記爲垃圾的對象進行回收了。怎麼樣,簡單吧。但是這種策略的缺點很明顯,
回收後內存碎片很多,如果之後程序運行時申請大內存,可能會又導致一次GC。
雖然缺點明顯,這種策略卻是後兩種策略的基礎。正因爲它的缺點,所以促成了
後兩種策略的產生。



(2)標記-複製

將內存分爲兩塊,標記完成開始回收時,將一塊內存中保留的對象全部複製到另
一塊空閒內存中。實現起來也很簡單,當大部分對象都被回收時這種策略也很高效。
但這種策略也有缺點,可用內存變爲一半了!

怎樣解決呢?聰明的程序員們總是辦法多過問題的。可以將堆不按1:1的比例分離,
而是按8:1:1分成一塊Eden和兩小塊Survivor區,每次將Eden和Survivor中存活的對象
複製到另一塊空閒的Survivor中。這三塊區域並不是堆的全部,而是構成了新生代

從下圖可以看到這三塊區域如何配合完成GC的,具體的對象空間分配以及晉升請
參加後面第6條補充。



爲什麼不是全部呢?如果回收時,空閒的那一小塊Survivor不夠用了怎麼辦?這就是
老年代的用處。當不夠用時,這些對象將直接通過分配擔保機制進入老年代。那麼
老年代也使用標記-複製策略吧?當然不行!老年代中的對象可不像新生代中的,
每次回收都會清除掉大部分。如果貿然採用複製的策略,老年代的回收效率可想而知。

(3)標記-整理

根據老年代的特點,採用回收掉垃圾對象後對內存進行整理的策略再合適不過,將
所有存活下來的對象都向一端移動。




4.實現:虛擬機中的收集器

(1)新生代上的GC實現

Serial:單線程的收集器,只使用一個線程進行收集,並且收集時會暫停其他所有
工作線程(Stop the world)。它是Client模式下的默認新生代收集器。

ParNew:Serial收集器的多線程版本。在單CPU甚至兩個CPU的環境下,由於線程
交互的開銷,無法保證性能超越Serial收集器。

Parallel Scavenge:也是多線程收集器,與ParNew的區別是,它是吞吐量優先
收集器。吞吐量=運行用戶代碼時間/(運行用戶代碼+垃圾收集時間)。另一點區別
是配置-XX:+UseAdaptiveSizePolicy後,虛擬機會自動調整Eden/Survivor等參數來
提供用戶所需的吞吐量。我們需要配置的就是內存大小-Xmx和吞吐量GCTimeRatio。

(2)老年代上的GC實現

Serial Old:Serial收集器的老年代版本。

Parallel Old:Parallel Scavenge的老年代版本。此前,如果新生代採用PS GC的話,
老年代只有Serial Old能與之配合。現在有了Parallel Old與之配合,可以在注重吞吐量
及CPU資源敏感的場合使用了。

CMS:採用的是標記-清除而非標記-整理,是一款併發低停頓的收集器。但是由於
採用標記-清除,內存碎片問題不可避免。可以使用-XX:CMSFullGCsBeforeCompaction
設置執行幾次CMS回收後,跟着來一次內存碎片整理。


5.觸發:何時開始GC?

Minor GC(新生代回收)的觸發條件比較簡單,Eden空間不足就開始進行Minor GC
回收新生代。而Full GC(老年代回收,一般伴隨一次Minor GC)則有幾種觸發條件:

(1)老年代空間不足

(2)PermSpace空間不足

(3)統計得到的Minor GC晉升到老年代的平均大小大於老年代的剩餘空間

這裏注意一點:PermSpace並不等同於方法區,只不過是Hotspot JVM用PermSpace來
實現方法區而已,有些虛擬機沒有PermSpace而用其他機制來實現方法區。


6.補充:對象的空間分配和晉升

(1)對象優先在Eden上分配

(2)大對象直接進入老年代

虛擬機提供了-XX:PretenureSizeThreshold參數,大於這個參數值的對象將直接分配到
老年代中。因爲新生代採用的是標記-複製策略,在Eden中分配大對象將會導致Eden區
和兩個Survivor區之間大量的內存拷貝。

(3)長期存活的對象將進入老年代

對象在Survivor區中每熬過一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度
(默認爲15歲)時,就會晉升到老年代中。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章