1. 程式人生 > >JAVA垃圾回收-可達性分析演算法

JAVA垃圾回收-可達性分析演算法

在java中是通過引用來和物件進行關聯的,也就是說如果要操作物件,必須通過引用來進行。那麼很顯然一個簡單的辦法就是通過引用計數來判斷一個物件是否可以被回收。不失一般性,如果一個物件沒有任何引用與之關聯,則說明該物件基本不太可能在其他地方被使用到,那麼這個物件就成為可被回收的物件了。這種方式成為引用計數法。

這種方式的特點是實現簡單,而且效率較高,但是它無法解決迴圈引用的問題,因此在Java中並沒有採用這種方式(Python採用的是引用計數法)。看下面這段程式碼:

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
 
        object1.object = object2;
        object2.object = object1;
 
        object1 = null;
        object2 = null;
    }
}
 
class MyObject{
    public Object object = null;
}

最後面兩句將object1和object2賦值為null,也就是說object1和object2指向的物件已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數都不為0,那麼垃圾收集器就永遠不會回收它們。


為了解決這個問題,在Java中採取了 可達性分析法。該方法的基本思想是通過一系列的“GC Roots”物件作為起點進行搜尋,如果在“GC Roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的,不過要注意的是被判定為不可達的物件不一定就會成為可回收物件。被判定為不可達的物件要成為可回收物件必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收物件的可能性,則基本上就真的成為可回收物件了。最後面兩句將object1和object2賦值為null,也就是說object1和object2指向的物件已經不可能再被訪問,但是由於它們互相引用對方,導致它們的引用計數都不為0,那麼垃圾收集器就永遠不會回收它們。

Java並不採用引用計數法來判斷物件是否已“死”,而採用“可達性分析”來判斷物件是否存活(同樣採用此法的還有C#、Lisp-最早的一門採用動態記憶體分配的語言)。 
此演算法的核心思想:通過一系列稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為“引用鏈”,當一個物件到 GC Roots 沒有任何的引用鏈相連時(從 GC Roots 到這個物件不可達)時,證明此物件不可用。以下圖為例: 

è¿éåå¾çæè¿°

物件Object5 —Object7之間雖然彼此還有聯絡,但是它們到 GC Roots 是不可達的,因此它們會被判定為可回收物件。

在Java語言中,可作為GC Roots的物件包含以下幾種:

  1. 虛擬機器棧(棧幀中的本地變量表)中引用的物件。(可以理解為:引用棧幀中的本地變量表的所有物件)
  2. 方法區中靜態屬性引用的物件(可以理解為:引用方法區該靜態屬性的所有物件)
  3. 方法區中常量引用的物件(可以理解為:引用方法區中常量的所有物件)
  4. 本地方法棧中(Native方法)引用的物件(可以理解為:引用Native方法的所有物件)

可以理解為:

(1)首先第一種是虛擬機器棧中的引用的物件,我們在程式中正常建立一個物件,物件會在堆上開闢一塊空間,同時會將這塊空間的地址作為引用儲存到虛擬機器棧中,如果物件生命週期結束了,那麼引用就會從虛擬機器棧中出棧,因此如果在虛擬機器棧中有引用,就說明這個物件還是有用的,這種情況是最常見的。

(2)第二種是我們在類中定義了全域性的靜態的物件,也就是使用了static關鍵字,由於虛擬機器棧是執行緒私有的,所以這種物件的引用會儲存在共有的方法區中,顯然將方法區中的靜態引用作為GC Roots是必須的。

(3)第三種便是常量引用,就是使用了static final關鍵字,由於這種引用初始化之後不會修改,所以方法區常量池裡的引用的物件也應該作為GC Roots。最後一種是在使用JNI技術時,有時候單純的Java程式碼並不能滿足我們的需求,我們可能需要在Java中呼叫C或C++的程式碼,因此會使用native方法,JVM記憶體中專門有一塊本地方法棧,用來儲存這些物件的引用,所以本地方法棧中引用的物件也會被作為GC Roots。

JVM之判斷物件是否存活(引用計數演算法、可達性分析演算法,最終判定)

finalize()方法最終判定物件是否存活:

    即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷再次標記過程。
    標記的前提是物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈。
  1).第一次標記並進行一次篩選。
    篩選的條件是此物件是否有必要執行finalize()方法。
    當物件沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”,物件被回收。

  2).第二次標記
    如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會被放置在一個名為:F-Queue的佇列之中,並在稍後由一條虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但並不承諾會等待它執行結束。這樣做的原因是,如果一個物件finalize()方法中執行緩慢,或者發生死迴圈(更極端的情況),將很可能會導致F-Queue佇列中的其他物件永久處於等待狀態,甚至導致整個記憶體回收系統崩潰。
    Finalize()方法是物件脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模標記,如果物件要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個物件建立關聯即可,譬如把自己賦值給某個類變數或物件的成員變數,那在第二次標記時它將移除出“即將回收”的集合。如果物件這時候還沒逃脫,那基本上它就真的被回收了。
流程圖如下:

在JDK1.2以前,Java中引用的定義很傳統: 如果引用型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。這種定義有些狹隘,一個物件在這種定義下只有被引用或者沒有被引用兩種狀態。 
我們希望能描述這一類物件: 當記憶體空間還足夠時,則能儲存在記憶體中;如果記憶體空間在進行垃圾回收後還是非常緊張,則可以拋棄這些物件。很多系統中的快取物件都符合這樣的場景。 
在JDK1.2之後,Java對引用的概念做了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)四種,這四種引用的強度依次遞減。

⑴強引用(StrongReference)
強引用是使用最普遍的引用。如果一個物件具有強引用,那垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足的問題。  ps:強引用其實也就是我們平時A a = new A()這個意思。

⑵軟引用(SoftReference)
如果一個物件只具有軟引用,則記憶體空間足夠,垃圾回收器就不會回收它;如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體(下文給出示例)。
軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

⑶弱引用(WeakReference)
弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。
弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。

⑷虛引用(PhantomReference)
“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。
虛引用主要用來跟蹤物件被垃圾回收器回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之 關聯的引用佇列中。

1 為什麼需要使用軟引用

首先,我們看一個僱員資訊查詢系統的例項。我們將使用一個Java語言實現的僱員資訊查詢系統查詢儲存在磁碟檔案或者資料庫中的僱員人事檔案資訊。作為一個使用者,我們完全有可能需要回頭去檢視幾分鐘甚至幾秒鐘前檢視過的僱員檔案資訊(同樣,我們在瀏覽WEB頁面的時候也經常會使用“後退”按鈕)。

這時我們通常會有兩種程式實現方式:

一種是:

把過去檢視過的僱員資訊儲存在記憶體中,每一個儲存了僱員檔案資訊的Java物件的生命週期貫穿整個應用程式始終;

另一種是:

當用戶開始檢視其他僱員的檔案資訊的時候,把儲存了當前所檢視的僱員檔案資訊的Java物件結束引用,使得垃圾收集執行緒可以回收其所佔用的記憶體空間,當用戶再次需要瀏覽該僱員的檔案資訊的時候,重新構建該僱員的資訊。

很顯然,第一種實現方法將造成大量的記憶體浪費.

而第二種實現的缺陷在於即使垃圾收集執行緒還沒有進行垃圾收集,包含僱員檔案資訊的物件仍然完好地儲存在記憶體中,應用程式也要重新構建一個物件。

我們知道,訪問磁碟檔案、訪問網路資源、查詢資料庫等操作都是影響應用程式執行效能的重要因素,如果能重新獲取那些尚未被回收的Java物件的引用,必將減少不必要的訪問,大大提高程式的執行速度。