目錄:
1、引用計數器算法
2、可達性分析算法
3、對象生死與引用的關系
4、死亡标記與拯救
但凡問到 JVM(Java 虛拟機)通常有 99% 的機率一定會問:在 JVM 中如何判斷一個對象的生死狀态?
本文就來聊聊這個問題,判斷對象的生死狀态的算法有以下幾個:
引用電腦判斷對象是否存活的算法是這樣的:給每一個對象設定一個引用計數器,每當有一個地方引用這個對象的時候,計數器就加1,與之相反,每當引用失效的時候就減1。
優點:實作簡單、性能高。
缺點:增減處理頻繁消耗cpu計算、計數器占用很多位浪費空間、最重要的缺點是無法解決循環引用的問題。
因為引用計數器算法很難解決循環引用的問題,是以主流的Java虛拟機都沒有使用引用計數器算法來管理記憶體。
-
public class ReferenceDemo {
-
public Object instance = null;
-
private static final int _1Mb = 1024 * 1024;
-
private byte[] bigSize = new byte[10 * _1Mb]; // 申請記憶體
-
public static void main(String[] args) {
-
System.out.println(String.format("開始:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));
-
ReferenceDemo referenceDemo = new ReferenceDemo();
-
ReferenceDemo referenceDemo2 = new ReferenceDemo();
-
referenceDemo.instance = referenceDemo2;
-
referenceDemo2.instance = referenceDemo;
-
System.out.println(String.format("運作:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));
-
referenceDemo = null;
-
referenceDemo2 = null;
-
System.gc(); // 手動觸發垃圾回收
-
System.out.println(String.format("結束:%d M",Runtime.getRuntime().freeMemory() / (1024 * 1024)));
-
}
-
}
運作的結果:
開始:117 M
運作中:96 M
結束:119 M
從結果可以看出,虛拟機并沒有因為互相引用就不回收它們,也側面說明了虛拟機并不是使用引用計數器實作的。
在主流的語言的主流實作中,比如Java、C#、甚至是古老的Lisp都是使用的可達性分析算法來判斷對象是否存活的。
這個算法的核心思路就是通過一些列的“GC Roots”對象作為起始點,從這些對象開始往下搜尋,搜尋所經過的路徑稱之為“引用鍊”。
當一個對象到GC Roots沒有任何引用鍊相連的時候,證明此對象是可以被回收的。如下圖所示:
在Java中,可作為GC Roots對象的清單:
- Java虛拟機棧中的引用對象。
- 本地方法棧中JNI(既一般說的Native方法)引用的對象。
- 方法區中類靜态常量的引用對象。
- 方法區中常量的引用對象。
對象生死與引用的關系
從上面的兩種算法來看,不管是引用計數法還是可達性分析算法都與對象的“引用”有關,這說明:對象的引用決定了對象的生死。
那對象的引用都有那些呢?
在JDK1.2之前,引用的定義很傳統:如果reference類型的資料中存儲的數值代表的是另一塊記憶體的起始位址,就稱這塊記憶體代表着一塊引用。
這樣的定義很純粹,但是也很狹隘,這種情況下一個對象要麼被引用,要麼沒引用,對于介于兩者之間的對象顯得無能為力。
JDK1.2之後對引用進行了擴充,将引用分為:
- 強引用(Strong Reference)
- 軟引用(Soft Reference)
- 弱引用(Weak Reference)
- 虛引用(Phantom Reference)
這也就是文章開頭第一個問題的答案,對象不是非生即死的,當空間還足夠時,還可以保留這些對象
如果空間不足時,再抛棄這些對象。很多緩存功能的實作也符合這樣的場景。
強引用、軟引用、弱引用、虛引用,這4種引用的強度是依次遞減的。
強引用:在代碼中普遍存在的,類似“Object obj = new Object()”這類引用,隻要強引用還在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用:是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,隻有當jvm認為記憶體不足時,才會去試圖回收軟引用指向的對象。jvm會確定在抛出OutOfMemoryError之前,清理軟引用指向的對象。
弱引用:非必需對象,但它的強度比軟引用更弱,被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。
虛引用:也稱為幽靈引用或幻影引用,是最弱的一種引用關系,無法通過虛引用來擷取一個對象執行個體,為對象設定虛引用的目的隻有一個,就是當着個對象被收集器回收時收到一條系統通知。
死亡标記與拯救
在可達性算法中不可達的對象,并不是“非死不可”的,要真正宣告一個對象死亡,至少要經曆兩次标記的過程。
如果對象在進行可達性分析之後,沒有與GC Roots相連接配接的引用鍊,它會被第一次标記,并進行篩選,篩選的條件是此對象是否有必要執行finalize()方法。
執行finalize()方法的兩個條件:
1、重寫了finalize()方法。
2、finalize()方法之前沒被調用過,因為對象的finalize()方法隻能被執行一次。
如果滿足以上兩個條件,這個對象将會放置在F-Queue的隊列之中,并在稍後由一個虛拟機自建的、低優先級Finalizer線程來執行它。
對象的“自我拯救”
finalize()方法是對象脫離死亡命運最後的機會,如果對象在finalize()方法中重新與引用鍊上的任何一個對象建立關聯即可,比如把自己(this關鍵字)指派給某個類變量或對象的成員變量。
來看具體的實作代碼:
-
public class FinalizeDemo {
-
public static FinalizeDemo Hook = null;
-
@Override
-
protected void finalize() throws Throwable {
-
super.finalize();
-
System.out.println("執行finalize方法");
-
FinalizeDemo.Hook = this;
-
}
-
public static void main(String[] args) throws InterruptedException {
-
Hook = new FinalizeDemo();
-
// 第一次拯救
-
Hook = null;
-
System.gc();
-
Thread.sleep(500); // 等待finalize執行
-
if (Hook != null) {
-
System.out.println("我還活着");
-
} else {
-
System.out.println("我已經死了");
-
}
-
// 第二次,代碼完全一樣
-
Hook = null;
-
System.gc();
-
Thread.sleep(500); // 等待finalize執行
-
if (Hook != null) {
-
System.out.println("我還活着");
-
} else {
-
System.out.println("我已經死了");
-
}
-
}
-
}
執行的結果:
執行finalize方法
我還活着
我已經死了
從結果可以看出,任何對象的finalize()方法都隻會被系統調用一次。
不建議使用finalize()方法來拯救對象,原因如下:
1、對象的finalize()隻能執行一次。
2、它的運作代價高昂。
3、不确定性大。
4、無法保證各個對象的調用順序。
END
歡迎長按下圖關注公衆号石杉的架構筆記,背景回複“資料”,擷取作者獨家秘制學習資料!