轉自: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日誌。
-
package com.cdai.jvm.gc;
-
-
public class ReferenceCount {
-
-
final static int MB = 1024 * 1024;
-
-
byte[] size = new byte[2 * MB];
-
-
Object ref;
-
-
public static void main(String[] args) {
-
ReferenceCount objA = new ReferenceCount();
-
ReferenceCount objB = new ReferenceCount();
-
objA.ref = objB;
-
objB.ref = objA;
-
-
objA = null;
-
objB = null;
-
-
System.gc();
-
System.gc();
-
}
-
-
}
[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()方法,
並使對象重新被引用。
-
package com.cdai.jvm.gc;
-
-
public class DeadToRebirth {
-
-
private static DeadToRebirth hook;
-
-
@Override
-
public void finalize() throws Throwable {
-
super.finalize();
-
DeadToRebirth.hook = this;
-
}
-
-
public static void main(String[] args) throws Exception {
-
DeadToRebirth.hook = new DeadToRebirth();
-
DeadToRebirth.hook = null;
-
System.gc();
-
Thread.sleep(500);
-
if (DeadToRebirth.hook != null)
-
System.out.println("Rebirth!");
-
else
-
System.out.println("Dead!");
-
-
DeadToRebirth.hook = null;
-
System.gc();
-
Thread.sleep(500);
-
if (DeadToRebirth.hook != null)
-
System.out.println("Rebirth!");
-
else
-
System.out.println("Dead!");
-
}
-
-
}
要注意的兩點是:
第一,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歲)時,就會晉升到老年代中。