jvm的垃圾回收是個老生常談的問題,在這裡,我會從以下一個方面來和大家聊聊垃圾回收。
1 在哪裡收垃圾?
2 哪些内容可認為是垃圾?
3 怎麼回收垃圾?
在哪裡收垃圾
這裡,我建議大家先讀一下拙作: java記憶體管理

上圖中的5部分:
虛拟機棧,本地方法棧,程式計數器三個區域随線程而生,随線程而滅;棧中的棧幀随着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每一個棧幀中配置設定多少記憶體基本上是在類結構确定下來時就已知的(盡管在運作期會由JIT編譯器進行一些優化,但在本章基于概念模型的讨論中,大體上可以認為是編譯期可知的),是以這幾個區域的記憶體配置設定和回收都具備确定性,在這幾個區域内不需要過多考慮回收的問題,因為方法結束或線程結束時,記憶體自然就跟随着回收了。
方法區,是各個線程所共享的。它存儲已被虛拟機加載的類資訊,常亮,靜态變量,即時編譯器編譯後的代碼等等。同時裡面還包含了運作時常量池, 用于存放編譯期間的各種字面量和符号引用。Java虛拟機規範中說過可以不要求虛拟機在方法區實作垃圾收集,而且在方法區進行垃圾收集的“成本效益”一般比較低。在堆中,尤其是在新生代中,正常應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低于此。
方法區永久代的垃圾收集主要回收兩部分内容:廢棄常量和無用的類。
方法區也成永久代(permanent generation)
最後剩下的就是堆了。
java堆是java虛拟機管理的最大一塊記憶體。且被所有線程共享。這塊區域在虛拟機啟動時就會建立。它存在的唯一目的就是存放對象。
我們主要就是在堆裡回收垃圾
我們先看看java中堆的組成
如上圖所示,整個堆被分為兩部分,新生代與老生代(
Tenured Gen)。
其中新生代又分為三部分,一個eden區(伊甸區,呵呵)和兩個Survivor區,分别是from Survivor與to Survivor。
各區域的預設比例在上圖中已經給出了。
那些内容可被認為是垃圾?
不能被通路到的對象就是垃圾
那麼我們如何判斷一個對象已經無法被通路到呢?這裡至少有兩種算法。
引用計數法
Person a=new Person("name1");
Person b=a;
pserson c=a;
c=new Person("name2");
在上面的代碼中,new Person在堆裡産生了一個對象,這個對象被引用了三次。後面c=new Person("name2"),原來的那個對象的被引用次數就較少一次,成了2。
我們可以給記錄每個對象被引用的次數。如果什麼時候,一個對象的被引用次數成了0,那麼我們就認為它是垃圾,可以被清除了。
這個方法OK嗎?我們看下面的例子
public class ReferenceCountingGC {
public static void main(String[] args) {
testGC();
}
public Object instance = null;
private static final int _1MB=1024*1024;
/**
* 這個成員屬性的唯一意義就是占點記憶體,以便能在GC日志中看清楚是否被回收過
*/
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountingGC objA=new ReferenceCountingGC();
ReferenceCountingGC objB=new ReferenceCountingGC();
objA.instance=objB;
objB.instance=objA;
objA=null;
objB=null;
// 假設在這行發生GC,那麼objA和objB是否能被回收?
System.gc();
}
}
邏輯上來說,obja與objb已經無法被通路到了,它就是垃圾#應該被清除,但是從引用計數法上來說,obj1與ojb2的被引用數都不為0,他們不應該被清除#
我們讀gc報告後,就能知道,這個兩個對象都已經被清除了,這就說明java并沒有用引用計數法來判定對象是否不可達#
(怎麼對gc的報告,一會再說)另外,java不用引用技術法并不能說明這個方法不好,Python就用引用法來管理
跟搜尋算法
這個方法邏輯上也很簡單
上圖中,從根可以到達object1234,但是object5,object6,object7是我們無法到達的。所有我們認為object5,6,7就是垃圾,就是應該回收的。
那麼問題來了,跟是什麼?
在Java語言裡,可作為GC Roots的對象包括下面幾種:
虛拟機棧(棧幀中的本地變量表)中的引用的對象。
方法區中的類靜态屬性引用的對象。
方法區中的常量引用的對象。
本地方法棧中JNI(即一般說的Native方法)的引用的對象。
怎麼回收垃圾(垃圾回收算法)
這裡也至少有三種算法
标記-回收算法
第一步:根據上面的跟搜尋算法,确定那些對象是應該清除的,并且标記這個對象
第二步:根據第一步做的标記,清除對象。
示意圖如下:
這算法的缺陷很明顯:有記憶體碎片的問題。
複制算法
算法的前提是:将記憶體區域分成兩部分,并且每次隻使用一部分
回收垃圾時
第一步:把正在使用部分中的存活對象,複制到第二塊記憶體上
第二步:将第一塊記憶體完全擦除。
示意圖如下:
這個算法的優勢在于:沒有碎片,但是劣勢但是卻對記憶體空間的使用做出了高昂的代價,因為能夠使用的記憶體縮減到原來的一半。而且複制算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麼Copying算法的效率将會大大降低。
标記整理算法
這個算法與标記-回收算法類似
第一步:标記出存活對象,垃圾對象。
第二步:将存活對象向記憶體的一端移動。
第三步:清除掉邊界外的記憶體。
示意圖如下:
分代回收法
堆的分代情況在上文已經說到了。
在新生代我們采用的是複制算法。
老生代采用的是标記-整理算法或标記清理算法。
這裡就牽扯了一個新問題,對象在記憶體中,是怎麼配置設定的?
如果是小對象,直接優先放到新生代的eden;如果是大對象,就直接放到老生代。
如果在新生代配置設定記憶體時,發現不夠用了,就使用複制算法,将eden和一個survivor區的存活對象複制到另一個suvivor區。
等于說是,用10%的區域來存放90%的内容。
這個能放下嗎?
大部分情況下,答案都是肯定的,因為ibm有研究表明新生代的對象有98%是朝生夕死的。
那如果隻占10%的survivor區域真的存不下90%區域中的存活對象呢?
答案是:把這些對象放到老年代中。
如果新生代的某個對象經曆了15次垃圾回收都沒有死,那把它也放到老生代裡。
那如果新生代,老生代都滿了呢?
不是還有一個異常叫OutOfMemoryError: Java heap space麼?
另外,垃圾收集的動作也有兩種:
新生代 GC(Minor GC):指發生在新生代的垃圾收集動作,因為 Java 對象大多都具備朝生夕滅的特性,是以 Minor GC 非常頻繁,一般回收速度也比較快。
老年代 GC(Major GC / Full GC):指發生在老年代的 GC,出現了 Major GC,經常會伴随至少一次的 Minor GC(但非絕對的,在 ParallelScavenge 收集器的收集政策裡就有直接進行 Major GC 的政策選擇過程)。MajorGC 的速度一般會比 Minor GC 慢 10倍以上。
那麼大對象,小對象的邊界是什麼?多大算大,多小算小呢?jvm有一個參數來設定這個值,大家有興趣的百度之。
下一節,我們看看幾個gc的執行個體及gc報告的閱讀
參考資料
深入了解java虛拟機 第三章
http://www.th7.cn/Program/java/201409/276272.shtml