1. 程式人生 > >關於強引用、軟引用、弱引用、幻象引用,你該如何回答?

關於強引用、軟引用、弱引用、幻象引用,你該如何回答?

我們說的不同的引用型別其實都是邏輯上的,而對於虛擬機器來說,主要體現的是物件的不同的`可達性(reachable)` 狀態和對`垃圾收集(garbage collector)`的影響。 ## 初識引用 對於剛接觸 Java 的 C++ 程式設計師而言,理解棧和堆的關係可能很不習慣。在 C++ 中,可以使用 new 操作符在堆上建立物件,或者使用自動分配在棧上建立物件。下面的 C++ 語句是合法的,但是 Java 編譯器卻拒絕這麼寫程式碼,會出現 `syntax error` 編譯錯誤。 ```java Integer foo = Integer(1); ``` Java 和 C 不一樣,Java 中會把物件都放在堆上,需要 new 操作符來建立物件。本地變數儲存在`棧`中,它們持有一個指向堆中物件的`引用(指標)`。下面是一個 Java 方法,該方法具有一個 Integer 變數,該變數從 String 解析值 ```java public static void foo(String bar){ Integer baz = new Integer(bar); } ``` 這段程式碼我們使用堆疊分配圖可以看一下它們的關係 ![](https://img2020.cnblogs.com/blog/1515111/202004/1515111-20200425181959328-304051317.png) 首先先來看一下 `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)` 的收集方式。但是這種方式無法解決物件之間相會引用的問題,如下 ```java 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; } } ``` 然而實際上,JVM 使用一種叫做 `標記-清除(Mark-Sweep)`的演算法,標記清除垃圾回收背後的想法很簡單:程式無法到達的每個物件都是垃圾,可以進行回收。 標記-清除收集具有如下幾個階段 * 階段一:標記 垃圾收集器會從 `根(root)` 引用開始,標記它到達的所有物件。如果用老師給學生判斷卷子來比喻,這就相當於是給試卷上的全部答案判斷正確還是錯誤的過程。 ![](https://img2020.cnblogs.com/blog/1515111/202004/1515111-20200425182008904-1654343562.png) * 階段二:清理 在第一階段中所有可回收的的內容都能夠被垃圾收集器進行回收。如果一個物件被判定為是可以回收的物件,那麼這個物件就被放在一個 `finalization queue(回收佇列)`中,並在稍後會由一個虛擬機器自動建立的、低優先順序的 `finalizer` 執行緒去執行它。 ![](https://img2020.cnblogs.com/blog/1515111/202004/1515111-20200425182015131-1670094241.png) * 階段三:整理(可選) 一些收集器有第三個步驟,整理。在這個步驟中,GC 將物件移動到垃圾收集器回收完物件後所留下的自由空間中。這麼做可以防止堆碎片化,防止大物件在堆中由於堆空間的不連續性而無法分配的情況。 ![](https://img2020.cnblogs.com/blog/1515111/202004/1515111-20200425182022415-1609544787.png) 所以上面的過程中就涉及到一個`根節點(GC Roots)` 來判斷是否存在需要回收的物件。這個演算法的基本思想就是通過一系列的 `GC Roots` 作為起始點,從這些節點向下搜尋,搜尋所走過的路徑稱為 `引用鏈(Reference Chain)`,當一個物件到 GC Roots 之間沒有任何引用鏈相連的話,則證明此物件不可用。引用鏈上的任何一個能夠被訪問的物件都是`強引用` 物件,垃圾收集器不會回收強引用物件。 因此,返回到 foo() 方法中,僅在執行方法時,引數 bar 和區域性變數 baz 才是強引用。一旦方法執行完成,它們都超過了作用域的時候,它們引用的物件都會進行垃圾回收。 下面來考慮一個例子 ```java LinkedList foo = new LinkedList(); foo.add(new Integer(111)); ``` 變數 foo 是一個強引用,它指向一個 LinkedList 物件。LinkedList(JDK.18) 是一個連結串列的資料結構,每一個元素都會指向前驅元素,每個元素都有其後繼元素。 ![](https://img2020.cnblogs.com/blog/1515111/202004/1515111-20200425182029906-467699465.png) 當我們呼叫`add()` 方法時,我們會增加一個新的連結串列元素,並且該連結串列元素指向值為 111 的 Integer 例項。這是一連串的強引用,也就是說,這個 Integer 的例項不符合垃圾收集條件。一旦 foo 物件超出了程式執行的作用域,LinkedList 和其中的引用內容都可以進行收集,收集的前提是沒有強引用關係。 ### Finalizers C++ 允許物件定義解構函式方法:當物件超出作用範圍或被明確刪除時,會呼叫解構函式來清理使用的資源。對於大多數物件來說,解構函式能夠釋放使用 new 或者 malloc 函式分配的記憶體。 在Java中,垃圾收集器會為你自動清除物件,分配記憶體,因此不需要顯式解構函式即可執行此操作。這也是 Java 和 C++ 的一大區別。 然而,記憶體並不是唯一需要被釋放的資源。考慮 `FileOutputStream`:當你建立此物件的例項時,它從作業系統分配檔案控制代碼。如果你讓流的引用在關閉前超過了其作用範圍,該檔案控制代碼會怎麼樣?實際上,每個流都會有一個 `finalizer` 方法,這個方法是垃圾回收器在回收之前由 JVM 呼叫的方法。對於 FileOutputStream 來說,**finalizer 方法會關閉流,釋放檔案控制代碼給作業系統,然後清除緩衝區,確保資料能夠寫入磁碟**。 任何物件都具有 finalizer 方法,你要做的就是宣告 `finalize()` 方法。如下 ```java protected void finalize() throws Throwable { // 清除物件 } ``` 雖然 finalizers 的 finalize() 方法是一種好的清除方式,但是這種方法產生的負面影響非常大,你不應該依靠這個方法來做任何垃圾回收工作。因為 `finalize` 方法的執行開銷比較大,不確定性強,無法保證各個物件的呼叫順序。finalize 能做的任何事情,可以使用 `try-finally` 或者其他方式來做,甚至做的更好。 ## 物件的生命週期 綜上所述,可以通過下面的流程來對物件的生命週期做一個總結 ![](https://img2020.cnblogs.com/blog/1515111/202004/1515111-20200425182038141-1201935650.png) 物件被建立並初始化,物件在執行時被使用,然後離開物件的作用域,物件會變成不可達並會被垃圾收集器回收。圖中用紅色標明的區域表示物件處於強可達階段。 JDK1.2 介紹了 `java.lang.ref` 包,物件的生命週期有四個階段:`