初步理解可達性算法和引用定義

在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對象的引用,必將減少不必要的訪問,大大提高程序的運行速度。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章