小心點,別被當成垃圾回收了。

我們說的不同的引用類型其實都是邏輯上的,而對於虛擬機來說,主要體現的是對象的不同的可達性(reachable) 狀態和對垃圾收集(garbage collector)的影響。

初識引用

對於剛接觸 Java 的 C++ 程序員而言,理解棧和堆的關係可能很不習慣。在 C++ 中,可以使用 new 操作符在堆上創建對象,或者使用自動分配在棧上創建對象。下面的 C++ 語句是合法的,但是 Java 編譯器卻拒絕這麼寫代碼,會出現 syntax error 編譯錯誤。

<code>Integer foo = Integer(1);/<code>

Java 和 C 不一樣,Java 中會把對象都放在堆上,需要 new 操作符來創建對象。本地變量存儲在棧中,它們持有一個指向堆中對象的引用(指針)。下面是一個 Java 方法,該方法具有一個 Integer 變量,該變量從 String 解析值

<code>public static void foo(String bar){
    Integer baz = new Integer(bar);
}/<code>

這段代碼我們使用堆棧分配圖可以看一下它們的關係

小心點,別被當成垃圾回收了。

首先先來看一下 foo() 方法,這一行代碼分配了一個新的 Integer 對象,JVM 嘗試在堆空間中開闢一塊內存空間。如果允許分配的話,就會調用 Integer 的構造方法把 String 字符串轉換為 Integer 對象。JVM 將指向該對象的指針存儲在變量 baz 中。

上面這種情況是我們樂意看到的情況,畢竟我們不想在編寫代碼的時候遇到阻礙,但是這種情況是不可能出現的,當堆空間無法為 bar 和 baz 開闢內存空間時,就會出現 OutOfMemoryError,然後就會調用垃圾收集器(garbage collector) 來嘗試騰出內存空間。這中間涉及到一個問題,垃圾收集器會回收哪些對象?

垃圾收集器

Java 給你提供了一個 new 操作符來為堆中的對象開闢內存空間,但它沒有提供 delete 操作符來釋放對象空間。當 foo() 方法返回時,如果變量 baz 超過最大內存,但它所指向的對象仍然還在堆中。如果沒有垃圾回收器的話,那麼程序就會拋出 OutOfMemoryError 錯誤。然而 Java 不會,它會提供垃圾收集器來釋放不再引用的對象。

當程序嘗試創建新對象並且堆中沒有足夠的空間時,垃圾收集器就開始工作。當收集器訪問堆時,請求線程被掛起,試圖查找程序不再主動使用的對象,並回收它們的空間。如果垃圾收集器無法釋放足夠的內存空間,並且JVM 無法擴展堆,則會出現 OutOfMemoryError,你的應用程序通常在這之後崩潰。還有一種情況是 StackOverflowError ,它出現的原因是因為線程請求的棧深度要大於虛擬機所允許的深度時出現的錯誤。

標記 - 清除算法

Java 能永久不衰的一個原因就是因為垃圾收集器。許多人認為 JVM 會為每個對象保留一個引用計數,當每次引用對象的時候,引用計數器的值就 + 1,當引用失效的時候,引用計數器的值就 - 1。而垃圾收集器只會回收引用計數器的值為 0 的情況。這其實是 引用計數法(Reference Counting) 的收集方式。但是這種方式無法解決對象之間相互引用的問題,如下

<code>class A{
  public B b;

}
class B{
  public A a;
}
public class Main{
    public static void main(String[] args){
    A a = new A();
    B b = new B();
    a.b=b;
    b.a=a;
    }
}/<code>

然而實際上,JVM 使用一種叫做 標記-清除(Mark-Sweep)的算法,標記清除垃圾回收背後的想法很簡單:程序無法到達的每個對象都是垃圾,可以進行回收。

標記-清除收集具有如下幾個階段

  • 階段一:標記

垃圾收集器會從 根(root) 引用開始,標記它到達的所有對象。如果用老師給學生判斷卷子來比喻,這就相當於是給試卷上的全部答案判斷正確還是錯誤的過程。

小心點,別被當成垃圾回收了。

  • 階段二:清理

在第一階段中所有可回收的的內容都能夠被垃圾收集器進行回收。如果一個對象被判定為是可以回收的對象,那麼這個對象就被放在一個 finalization queue(回收隊列)中,並在稍後會由一個虛擬機自動建立的、低優先級的 finalizer 線程去執行它。

小心點,別被當成垃圾回收了。

  • 階段三:整理(可選)

一些收集器有第三個步驟,整理。在這個步驟中,GC 將對象移動到垃圾收集器回收完對象後所留下的自由空間中。這麼做可以防止堆碎片化,防止大對象在堆中由於堆空間的不連續性而無法分配的情況。

小心點,別被當成垃圾回收了。

所以上面的過程中就涉及到一個根節點(GC Roots) 來判斷是否存在需要回收的對象。這個算法的基本思想就是通過一系列的 GC Roots 作為起始點,從這些節點向下搜索,搜索所走過的路徑稱為 引用鏈(Reference Chain),當一個對象到 GC Roots 之間沒有任何引用鏈相連的話,則證明此對象不可用。引用鏈上的任何一個能夠被訪問的對象都是強引用 對象,垃圾收集器不會回收強引用對象。

因此,返回到 foo() 方法中,僅在執行方法時,參數 bar 和局部變量 baz 才是強引用。一旦方法執行完成,它們都超過了作用域的時候,它們引用的對象都會進行垃圾回收。

下面來考慮一個例子

<code>LinkedList foo = new LinkedList();
foo.add(new Integer(111));/<code>

變量 foo 是一個強引用,它指向一個 LinkedList 對象。LinkedList(JDK.18) 是一個鏈表的數據結構,每一個元素都會指向前驅元素,每個元素都有其後繼元素。

小心點,別被當成垃圾回收了。

當我們調用add() 方法時,我們會增加一個新的鏈表元素,並且該鏈表元素指向值為 111 的 Integer 實例。這是一連串的強引用,也就是說,這個 Integer 的實例不符合垃圾收集條件。一旦 foo 對象超出了程序運行的作用域,LinkedList 和其中的引用內容都可以進行收集,收集的前提是沒有強引用關係。

Finalizers

C++ 允許對象定義析構函數方法:當對象超出作用範圍或被明確刪除時,會調用析構函數來清理使用的資源。對於大多數對象來說,析構函數能夠釋放使用 new 或者 malloc 函數分配的內存。在Java中,垃圾收集器會為你自動清除對象,分配內存,因此不需要顯式析構函數即可執行此操作。這也是 Java 和 C++ 的一大區別。

然而,內存並不是唯一需要被釋放的資源。考慮 FileOutputStream:當你創建此對象的實例時,它從操作系統分配文件句柄。如果你讓流的引用在關閉前超過了其作用範圍,該文件句柄會怎麼樣?實際上,每個流都會有一個 finalizer 方法,這個方法是垃圾回收器在回收之前由 JVM 調用的方法。對於 FileOutputStream 來說,finalizer 方法會關閉流,釋放文件句柄給操作系統,然後清除緩衝區,確保數據能夠寫入磁盤

任何對象都具有 finalizer 方法,你要做的就是聲明 finalize() 方法。如下

<code>protected void finalize() throws Throwable{
    // 清除對象
}/<code>

雖然 finalizers 的 finalize() 方法是一種好的清除方式,但是這種方法產生的負面影響非常大,你不應該依靠這個方法來做任何垃圾回收工作。因為 finalize 方法的運行開銷比較大,不確定性強,無法保證各個對象的調用順序。finalize 能做的任何事情,可以使用 try-finally 或者其他方式來做,甚至做的更好。

對象的生命週期

綜上所述,可以通過下面的流程來對對象的生命週期做一個總結

小心點,別被當成垃圾回收了。

對象被創建並初始化,對象在運行時被使用,然後離開對象的作用域,對象會變成不可達並會被垃圾收集器回收。圖中用紅色標明的區域表示對象處於強可達階段。

JDK1.2 介紹了 java.lang.ref 包,對象的生命週期有四個階段:強可達(Strongly Reachable)、軟可達(Soft Reachable)、弱可達(Weak Reachable)、 幻象可達(Phantom Reachable)。

小心點,別被當成垃圾回收了。

如果只討論符合垃圾回收條件的對象,那麼只有三種:軟可達、弱可達和幻象可達。

  • 軟可達:軟可達就是我們只能通過軟引用才能訪問的狀態,軟可達的對象是由 SoftReference 引用的對象,並且沒有強引用的對象。軟引用是用來描述一些還有用但是非必須的對象。垃圾收集器會盡可能長時間的保留軟引用的對象,但是會在發生 OutOfMemoryError 之前,回收軟引用的對象。如果回收完軟引用的對象,內存還是不夠分配的話,就會直接拋出 OutOfMemoryError。
  • 弱可達:弱可達的對象是 WeakReference 引用的對象。垃圾收集器可以隨時收集弱引用的對象,不會嘗試保留軟引用的對象。
  • 幻象可達:幻象可達是由 PhantomReference 引用的對象,幻象可達就是沒有強、軟、弱引用進行關聯,並且已經被 finalize 過了,只有幻象引用指向這個對象的時候。

除此之外,還有強可達和不可達的兩種可達性判斷條件

  • 強可達:就是一個對象剛被創建、初始化、使用中的對象都是處於強可達的狀態
  • 不可達(unreachable):處於不可達的對象就意味著對象可以被清除了。

下面是一個不同可達性狀態的轉換圖

小心點,別被當成垃圾回收了。

判斷可達性條件,也是 JVM 垃圾收集器決定如何處理對象的一部分考慮因素。

所有的對象可達性引用都是 java.lang.ref.Reference 的子類,它裡面有一個get() 方法,返回引用對象。如果已通過程序或垃圾收集器清除了此引用對象,則此方法返回 null 。也就是說,除了幻象引用外,軟引用和弱引用都是可以得到對象的。而且這些對象可以人為拯救,變為強引用,例如把 this 關鍵字賦值給對象,只要重新和引用鏈上的任意一個對象建立關聯即可。

ReferenceQueue

引用隊列又稱為 ReferenceQueue,它位於 java.lang.ref 包下。我們在建各種引用(軟引用,弱引用,幻象引用)並關聯到響應對象時,可以選擇是否需要關聯引用隊列。JVM 會在特定的時機將引用入隊到隊列中,程序可以通過判斷引用隊列中是否已經加入引用,來了解被引用的對象是否被GC回收。

Reference

java.lang.ref.Reference 為軟(soft)引用、弱(weak)引用、虛(phantom)引用的父類。因為 Reference 對象和垃圾回收密切配合實現,該類可能不能被直接子類化。


分享到:


相關文章: