jvm垃圾回收(GC)機制之如何判斷對象已死(哪些內存需要回收?)
1、爲什麼需要垃圾回收?
- 如果不進行垃圾回收,內存遲早都會被消耗空,因爲我們在不斷的分配內存空間而不進行回收。除非內存無限大,我們可以任性的分配而不回收,但是事實並非如此。所以,垃圾回收是必須的。
2、哪些內存需要回收?
- 哪些內存需要回收是垃圾回收機制第一個要考慮的問題,所謂“要回收的垃圾”無非就是那些不可能再被任何途徑使用的對象。那麼如何找到這些對象?
一、引用計數法
- 這個算法的實現是,給對象中添加一個引用計數器,每當一個地方引用這個對象時,計數器值+1;當引用失效時,計數器值-1。任何時刻計數值爲0的對象就是不可能再被使用的。這種算法使用場景很多,但是,Java中卻沒有使用這種算法,因爲這種算法很難解決對象之間相互引用的情況。看一段代碼:
/**
* 虛擬機參數:-verbose:gc
*/
public class ReferenceCountingGC
{
private Object instance = null;
private static final int _1MB = 1024 * 1024;
/** 這個成員屬性唯一的作用就是佔用一點內存 */
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args)
{
ReferenceCountingGC objectA = new ReferenceCountingGC();
ReferenceCountingGC objectB = new ReferenceCountingGC();
objectA.instance = objectB;
objectB.instance = objectA;
objectA = null;
objectB = null;
System.gc();
}
}
看下運行結果:
[GC 4417K->288K(61440K), 0.0013498 secs]
[Full GC 288K->194K(61440K), 0.0094790 secs]
看到,兩個對象相互引用着,但是虛擬機還是把這兩個對象回收掉了,這也說明虛擬機並不是通過引用計數法來判定對象是否存活的。(此算法過時)
二、可達性分析法
-
這個算法的基本思想是通過一系列稱爲“GC Roots”的對象作爲起始點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達)時,則證明此對象是不可用的。
-
那麼問題又來了,如何選取GCRoots對象呢?在Java語言中,可以作爲GCRoots的對象包括下面幾種:
(1). 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表)中引用的對象。
(2). 方法區中的類靜態屬性引用的對象。
(3). 方法區中常量引用的對象。
(4). 本地方法棧中JNI(Native方法)引用的對象。
下面給出一個GCRoots的例子,如下圖,爲GCRoots的引用鏈。
由圖可知,obj8、obj9、obj10都沒有到GCRoots對象的引用鏈,即便obj9和obj10之間有引用鏈,他們還是會被當成垃圾處理,可以進行回收。
三、四種引用狀態
- 在JDK1.2之前,Java中引用的定義很傳統:如果引用類型的數據中存儲的數值代表的是另一塊內存的起始地址,就稱這塊內存代表着一個引用。這種定義很純粹,但是太過於狹隘,一個對象只有被引用或者沒被引用兩種狀態。我們希望描述這樣一類對象:當內存空間還足夠時,則能保留在內存中;如果內存空間在進行垃圾收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用4種,這4種引用強度依次減弱。
1、強引用
代碼中普遍存在的類似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
2、軟引用
描述有些還有用但並非必需的對象。在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍進行二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。Java中的類SoftReference表示軟引用。
3、弱引用
描述非必需對象。被弱引用關聯的對象只能生存到下一次垃圾回收之前,垃圾收集器工作之後,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。Java中的類WeakReference表示弱引用。
4、虛引用
這個引用存在的唯一目的就是在這個對象被收集器回收時收到一個系統通知,被虛引用關聯的對象,和其生存時間完全沒關係。Java中的類PhantomReference表示虛引用。
- 對於可達性分析算法而言,未到達的對象並非是“非死不可”的,若要宣判一個對象死亡,至少需要經歷兩次標記階段。
1、 如果對象在進行可達性分析後發現沒有與GCRoots相連的引用鏈,則該對象被第一次標記並進行一次篩選,篩選條件爲是否有必要執行該對象的finalize方法,若對象沒有覆蓋finalize方法或者該finalize方法是否已經被虛擬機執行過了,則均視作不必要執行該對象的finalize方法,即該對象將會被回收。反之,若對象覆蓋了finalize方法並且該finalize方法並沒有被執行過,那麼,這個對象會被放置在一個叫F-Queue的隊列中,之後會由虛擬機自動建立的、優先級低的Finalizer線程去執行,而虛擬機不必要等待該線程執行結束,即虛擬機只負責建立線程,其他的事情交給此線程去處理。
2、對F-Queue中對象進行第二次標記,如果對象在finalize方法中拯救了自己,即關聯上了GCRoots引用鏈,如把this關鍵字賦值給其他變量,那麼在第二次標記的時候該對象將從“即將回收”的集合中移除,如果對象還是沒有拯救自己,那就會被回收。如下代碼演示了一個對象如何在finalize方法中拯救了自己,然而,它只能拯救自己一次,第二次就被回收了。具體代碼如下:
package com.demo;
/*
* 此代碼演示了兩點:
* 1.對象可以再被GC時自我拯救
* 2.這種自救的機會只有一次,因爲一個對象的finalize()方法最多隻會被系統自動調用一次
* */
public class FinalizeEscapeGC {
public String name;
public static FinalizeEscapeGC SAVE_HOOK = null;
public FinalizeEscapeGC(String name) {
this.name = name;
}
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
System.out.println(this);
FinalizeEscapeGC.SAVE_HOOK = this;
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC("leesf");
System.out.println(SAVE_HOOK);
// 對象第一次拯救自己
SAVE_HOOK = null;
System.out.println(SAVE_HOOK);
System.gc();
// 因爲finalize方法優先級很低,所以暫停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead : (");
}
// 下面這段代碼與上面的完全相同,但是這一次自救卻失敗了
// 一個對象的finalize方法只會被調用一次
SAVE_HOOK = null;
System.gc();
// 因爲finalize方法優先級很低,所以暫停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead : (");
}
}
}
運行結果如下:
leesf
null
finalize method executed!
leesf
yes, i am still alive :)
no, i am dead : (
由結果可知,該對象拯救了自己一次,第二次沒有拯救成功,因爲對象的finalize方法最多被虛擬機調用一次。此外,從結果我們可以得知,一個堆對象的this(放在局部變量表中的第一項)引用會永遠存在,在方法體內可以將this引用賦值給其他變量,這樣堆中對象就可以被其他變量所引用,即不會被回收。
四、方法區的垃圾回收:
-
方法區的垃圾回收主要回收兩部分內容:1. 廢棄常量。2. 無用的類。既然進行垃圾回收,就需要判斷哪些是廢棄常量,哪些是無用的類。
如何判斷廢棄常量呢?以字面量回收爲例,如果一個字符串“abc”已經進入常量池,但是當前系統沒有任何一個String對象引用了叫做“abc”的字面量,那麼,如果發生垃圾回收並且有必要時,“abc”就會被系統移出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。
如何判斷無用的類呢?需要滿足以下三個條件
1、該類的所有實例都已經被回收,即Java堆中不存在該類的任何實例。
2、 加載該類的ClassLoader已經被回收。
3、 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
- 滿足以上三個條件的類可以進行垃圾回收,但是並不是無用就被回收,虛擬機提供了一些參數供我們配置。
jvm虛擬機參數