Java筆記 — 物件參照級別之分析 (二)

Carl
11 min readMar 10, 2019

--

前一篇簡單地介紹了一下物件的 life cycle, 這篇會簡單介紹一下從 java.lang.ref.Reference 這個抽象類別衍生出來的概念: Reachability (可達性).

關於 Reachability (可達性)

從 JDK 1.2 開始, 引入了 java.lang.ref package, 此時在 Java 物件的生命週期裡可以分成以下四種狀態:

  • Strongly reachable
    這就是我們最常使用到的參照, 譬如在單一的執行緒裡透過 new 關鍵字建立物件 (e.g.: SomeType someType = new SomeType();), 此時我們就會稱此物件是一種 strong reference (對當下宣告此參照的執行緒來說). 只要還有 strong reference 指向一個物件, GC 就不會主動去回收這類物件. 除非此物件超過了參照的作用域或是其參照被指定為 null.
    基本上, strong reference 是不會被 GC 的, 且在 java.lang.ref package 裡也沒有相應的類別可以對應 (因為這在 JDK 1.2 前就是既存的事實了).
    對 strong reference 物件來說, GC 不會去回收它, 這意味著 JVM 寧可在記憶體不足的時候拋出 OutOfMemoryError, 讓程式終止, 也不願意靠著回收這類參照物件來解決記憶體不足的窘境.
  • Softly reachable
    物件本身會成為 SoftReference 的 referent (即 SoftReference 指向的物件), 在分類層級上, 是僅次於 strong reference 的一種狀態. 對 GC 來說, 若記憶體空間還夠, 就不會去回收這類物件, 通常會等到快要發生 OutOfMemoryError 之前才會來清理 SoftReference 的 referent. SoftReference 通常用來實作對記憶體需求比較敏感的快取(常見的如 Android 裡面, 對圖片的快取), 若還有可用的記憶體, 就可以暫時保留快取, 當記憶體不夠的時候再來進行清理的動作, 這樣就可以確保在使用快取的時候不會把記憶體用光.
  • Weakly reachable
    WeakReference 又比 SoftReference 更弱一點, 主要是其無法使物件豁免於被 GC 的命運. 其主要是提供一種手段讓我們可以去存取那些處於 weakly reachable 狀態下的物件. 講白一點, WeakReference 可以用來參照一個物件, 但是並不會阻止被參照的物件被 JVM 給回收掉. 我們知道, 當使用 strong reference 的物件時, 此物件是不會被回收掉的, 但使用 WeakReference 就沒有這個問題了. GC 發生的時候, 若一個物件的參照都是 weak reference 的話, 那這個物件就會被回收掉.
    WeakReference 解決了 strong reference 所產生的物件之間在存活時間上的耦合關係. 常見的使用場景像是在使用 collection 的時候: 假設今天我們使用的是一個 hash table, 我們都知道 hash table 接受的是一個 key-value pair, 而 Java 本身允許我們將任何的物件當作 key 來使用. 故當一對 key-value pair 被放入 hash table 裡以後, hash table 物件本身就有了對這些 key/value 物件的參照, 若這種參照是 strong reference 的話, 意味著只要這個 hash table 物件還存在, 其中所包含的 key-value pair 都不會被 JVM 回收掉. 其背後的意義就是: 當某個 hash table 物件裡包含了很大量的 key-value pair, 且其存活時間又很長的時候, JVM 的記憶體就有可能會被這個 hash table 給消耗殆盡.
    要解決這種情況, 我們就可以使用 WeakReference 來參照這些物件, 如此一來, hash table 中的 key-value pair 就可以被 GC 了. 這同樣是用來實作快取的一種選擇.
    JDK 裡提供了一種相關的 collection 實作, 可以參閱 WeakHashMap.
  • Phantom reachable
    Java 不同於 C++, 沒有解構子 (destructor) 這種機制, 但是可以使用 finalize 方法來達到類似的效果, 其初衷是希望物件在被回收之前, 能夠做一些事前清理的作業. 然而, GC 的時間基本上不會是固定的, 所以這些清理工作通常也無法被預知. PhantomReference 基本上可以在這部分達到一些輔助的效果: 通常在宣告一個 PhantomReference 物件時, 都必須顯式地指定一個 ReferenceQueue 作為參數之一, 當一個物件的 finalize 被呼叫之後, PhantomReference 的 referent 就會被加入到 ReferenceQueue 裡, 這時候只要去檢查這個 ReferenceQueue 就可以知道一個物件是否已經準備好要被回收了.
    另外要注意的是, 我們無法透過 PhantomReference 來存取保存在其中的 referent (下一小節會有原始碼展示), 如前所述, 其本身僅僅就只是提供了一種確保物件被 finalize 後, 能再進行某些處理的機制. 有些人可能也會利用 PhantomReference 來監控物件的建立與銷毀.
    PhantomReference 的使用狀況並不多見, 主要用來實作比較細部的記憶體操作, 通常這在移動裝置上會比較看得出效果. 程式可以在確定一個物件被回收之後, 再申請記憶體來建立新的物件. 這樣子可以將消耗的記憶體保持在一種較為穩定的數量.

可達性狀態流程分析

前面簡單說明了各種 reachability 的意思, 以下這張圖則是 reachability 狀態的變化流程圖:

Reachability State Flow

為什麼這張圖會長這樣呢? 可以這樣想:

  • Strongly Reachable
    這個意思是說, 當一個或多個執行緒可以在不通過各種參照直接存取到某個物件的情況. 譬如說你在當下的執行緒裡建立了一個物件, 那對建立這個物件的執行緒來說, 其對該物件就是 strongly reachable.
  • Softly Reachable
    就是你只能通過 SoftReference 才能夠存取到該物件 (referent) 的狀態.
  • Weakly Reachable
    連 SoftReference 都存取不到, 只能通過 WeakReference 才有辦法存取到該物件的狀態. 基本上這時候的物件已經很逼近 finalize 了.
  • Phantom Reachable
    參見上圖, 沒有 strong/soft/weak reference 關聯到該物件上, 且經過 finalize 階段, 此時就是處於 phantom reachable 狀態了.

除了 strong reference 之外, 所有的參照類型, 都是 java.lang.ref.Reference 的子類別, 其中提供了一個 get() 方法, 下圖以 SoftReference 的實作為例:

get() of SoftReference

可以觀察到其會試圖回傳建立 SoftReference 時, 保存的 referent.

再來看一下 PhantomReference 的實作:

get of PhantomReference

… 看來就只是個黑洞而已, 怎麼叫都不會給你東西.

對 java.lang.ref.Reference 的子類別來說, 這個方法除了 PhantomReference 之外 (如上圖, 透過它去呼叫 get 都只會回給你 null), 只要物件還沒有被銷毀, 都可以通過 get() 取得原有的物件. 這代表著: 利用 SoftReference 或著是 WeakReference, 我們可以將存取到的物件重新地指向強引用, 也就是透過人為介入去改變物件的可達狀態, 這就是為什麼上面的物件可達狀態圖裡面, 有些地方會出現雙向箭頭的原因.

所以對於軟引用跟弱引用, GC 可能會有二次確認的問題, 以保證處於弱引用狀態的物件沒有被改變為強引用.

這裡可能會出現一種問題: 如果我們重新地把 SoftReference/WeakReference 裡面的 referent 重新指向給 strong reference, 如 static 變數, 那這個物件基本上就沒有可能再回到類似 softly/weakly reachable 的狀態了, 就表示會發生 memory leak.

實際操作 Reference.get()

以下透過幾個簡單的情境來試試看 get() 這個方法 (以 SoftReference 的實作為範例).

  1. 指向物件的所有參照都沒有被釋放掉的狀況:
全都沒放掉

此時的記憶體使用狀態如下 (debug mode):

可以發現, 我們還是拿得到原來的物件(ListNode@1254)

2. 指向物件的部分參照被釋放掉的狀況:

部分放掉

此時的記憶體使用狀態如下 (debug mode):

看來還是拿得到原來的物件(ListNode@1257)

3. 指向物件的所有參照都被釋放掉的狀況:

放掉的前一行

注意原本的物件是 (ListNode@1268)

要開始放囉

此時的記憶體使用狀態如下 (debug mode):

程式執行到 45 行之後, 可以發現還是有辦法透過 get() 取得原來的物件 (ListNode@1268), 因為我們只有把外面的參照放掉, 但是 SoftReference 裡面還是有一個 referent 變數用來記憶原來的物件. 所以這時候你就可以透過人工的方式來把這個物件救回來, 譬如把它改成強引用狀態.

4. 指向物件的所有參照都被釋放掉的狀況, 然後 SoftReference 本身也進行清理:

全都清掉

呼叫 clear 的話, 就會把 Reference object 本身紀錄的 referent 給清除了.

此時的記憶體使用狀態如下 (debug mode):

什麼都沒了, 要救也沒機會了.

5. PhantomReference 的行為:

此時的記憶體使用狀態如下 (debug mode):

反正不管怎樣它都不會給你東西的.

關於 ReferenceQueue

前面看到了在建立 PhantomReference 物件時, 要提供一個 ReferenceQueue 物件作為建構子參數. 關於這部分, 其實在建立其它類型的 Reference Object (SoftReference/WeakReference) 的時候, 也是可以給定 ReferenceQueue的. 當我們有給定 ReferenceQueue 的時候, JVM 就會在特定的時間點將 referent 給 enqueue 到 ReferenceQueue 之中, 然後我們就可以從 queue 裡取得 referent (透過 remove 方法) 並且進行相關處理. 特別是對於 PhantomReference 來說, 其建構子要求一定要給定一個 ReferenceQueue, 畢竟其本身的 get() 實作都只會回傳 null 了, 再不給 ReferenceQueue, 還真的不知道 PhantomReference 能有什麼用了. 以下片段的程式碼就示範了要怎麼使用 ReferenceQueue:

如範例所示, 我們可以在物件轉移至相應狀態時 (Phantom reachable), 執行特定的邏輯處理.

References

--

--

Carl

Stand for something or you will fall for anything.