好的文章是能把各個知識點,通過邏輯關係串連起來,讓人豁然開朗的同時又記憶深刻。
導讀:物件除了生死之外,還有其他狀態嗎?物件真正的死亡,難道只經歷一次簡單的判定?如何在垂死的邊緣“拯救”一個將死物件?判斷物件的生死存活都有那些演算法?本文帶你一起找到這些答案。
在正式開始之前,我們先來了解一下垃圾回收。
GC介紹
GC: Garbage Collection,中文翻譯為垃圾回收。
GC的歷史
GC有著很長的歷史了,最初的GC演算法釋出於1960年(已經快有60年的歷史了),Lisp之父John McCarthy釋出的,他是一名非常有名的黑客,也是人工智慧之父,同時也是GC之父。
為什麼要學習GC?
1、排查記憶體溢位和記憶體洩露的問題。
2、系統調優,處理更高的併發瓶頸。
GC的作用
1、找到記憶體空間的垃圾。
2、回收垃圾。
物件生死判斷演算法
垃圾回收的第一步就是判斷物件是否存活,只有“死去”的物件,才會被垃圾回收器所收回。
引用計數器演算法
引用計算器判斷物件是否存活的演算法是這樣的:給每一個物件設定一個引用計數器,每當有一個地方引用這個物件的時候,計數器就加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、無法保證各個物件的呼叫順序。
參考
《深入理解Java虛擬機器》
《垃圾回收的演算法與實現》
※ 為寫好一篇技術文章,背後是讀了兩本書的“艱辛”。寫作不易,請多支援!!!
最後
關注公眾號,傳送“gc”關鍵字,領取《垃圾回收的演算法與實現》學習資料。