JVM 系列(6)吊打面試官:為什麼 finalize() 方法只會執行一次?
theme: jzman
攜手創作,共同成長!這是我參與「掘金日新計劃 · 8 月更文挑戰」的第7天,點選檢視活動詳情
請點贊關注,你的支援對我意義重大。
🔥 Hi,我是小彭。本文已收錄到 GitHub · AndroidFamily 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,關注公眾號 [彭旭銳] 帶你建立核心競爭力。
前言
Java Finalizer 機制提供了一個在物件被回收之前釋放佔用資源的時機,但是都說 Finalizer 機制是不穩定且危險的,不推薦使用,這是為什麼呢?今天我們來深入理解這個問題。
這篇文章是 JVM 系列文章第 6 篇,專欄文章列表:
一、記憶體管理:
- 1、記憶體區域劃分
- 2、垃圾回收機制
- 3、物件建立過程
- 4、物件記憶體佈局
- 5、引用型別
- 6、Finalizer 機制
二、編譯連結過程
- 1、Java 編譯過程
- 2、Class 檔案格式
- 3、註解處理器
- 4、註解機制
- 5、類載入機制
- 6、泛型機制
三、執行系統
- 1、方法呼叫與返回
- 2、過載與重寫
- 3、反射機制
- 4、異常機制
提示:很多內容都已經發表過了,最近會整理出來
學習路線圖:
1. 認識 Finalizer 機制
1.1 為什麼要使用 Finalizer 機制?
Java 的 Finalizer 機制的作用在一定程度上是跟 C/C++ 解構函式類似的機制。當一個物件的生命週期即將終結時,也就是即將被垃圾收集器回收之前,虛擬機器就會呼叫物件的 finalize() 方法,從而提供了一個釋放資源的時機。
1.2 Finalizer 存在的問題
雖然 Java Finalizer 機制是起到與 C/C++ 解構函式類似的作用,但兩者的定位是有差異的。C/C++ 解構函式是回收物件資源的正常方式,與建構函式是一一對應的,而 Java Finalizer 機制是不穩定且危險的,不被推薦使用的,因為 Finalizer 機制存在以下 3 個問題:
- 問題 1 - Finalizer 機制執行時機不及時: 由於執行 Finalizer 機制的執行緒是一個守護執行緒,它的執行優先順序是比使用者執行緒低的,所以當一個物件變為不可達物件後,不能保證一定及時執行它的 finalize() 方法。因此,當大量不可達物件的 Finalizer 機制沒有及時執行時,就有可能造成大量資源來不及釋放,最終耗盡資源;
- 問題 2 - Finalizer 機制不保證執行: 除了執行時機不穩定,甚至不能保證 Finalizer 機制一定會執行。當程式結束後,不可達物件上的 Finalizer 機制有可能還沒有執行。假設程式依賴於 Finalizer 機制來更新持久化狀態,例如釋放資料庫的鎖,就有可能使得整個分散式系統陷入死鎖;
- 問題 3 - Finalizer 機制只會執行一次: 如果不可達物件在 finalize() 方法中被重新啟用為可達物件,那麼在它下次變為不可達物件後,不會再次執行 finalize() 方法。這與 Finalizer 機制的實現原理有關,後文我們將深入虛擬機器原始碼,從原始碼層面深入理解。
1.3 什麼時候使用 Finalizer 機制?
由於 Finalizer 機制存在不穩定性,因此不應該將 Finalizer 機制作為釋放資源的主要策略,而應該作為釋放資源的兜底策略。程式應該在不使用資源時主動釋放資源,或者實現 AutoCloseable
介面並通過 try-with-resources
語法確保在有異常的情況下依然會釋放資源。而 Finalizer 機制作為兜底策略,雖然不穩定但也好過忘記釋放資源。
不過,Finalizer 機制已經被標記為過時,使用 Cleaner 機制作為釋放資源的兜底策略(本質上是 PhantomReference 虛引用)是相對更好的選擇。雖然 Cleaner 機制也存在相同的不穩定性,但總體上比 Finalizer 機制更好。
2. Finalizer 機制原理分析
從這一節開始,我們來深入分析 Java Finalizer 機制的實現原理,相關原始碼基於 Android 9.0 ART 虛擬機器。
2.1 引用實現原理回顧
在上一篇文章中,我們分析過 Java 引用型別的實現原理,Java Finalizer 機制也是其中的一個環節,我們先對整個過程做一次簡單回顧。
2.2 建立 FinalizerReference 引用物件
我們都知道 Java 有四大引用型別,除此之外,虛擬機器內部還設計了 @hide
的 FinalizerReference
型別來支援 Finalizer 機制。Reference
引用物件是用來實現更加靈活的物件生命週期管理而設計的物件包裝類,Finalizer 機制也與物件的生命週期有關,因此存在這樣 “第 5 種引用型別” 也能理解。
在虛擬機器執行類載入的過程中,會將重寫了 Object#finalize()
方法的型別標記為 finalizable
型別。每次在建立標記為 finalizable 的物件時,虛擬機器內部會同時建立一個關聯的 FinalizerReference 引用物件,並將其暫存到一個全域性的連結串列中 (如果不存在全域性的變數中,沒有強引用持有的 FinalizerReference 本身在下次 GC 直接就被回收了)。
cpp
void Heap::AddFinalizerReference(Thread* self, ObjPtr<mirror::Object>* object) {
ScopedObjectAccess soa(self);
ScopedLocalRef<jobject> arg(self->GetJniEnv(), soa.AddLocalReference<jobject>(*object));
jvalue args[1];
args[0].l = arg.get();
// 呼叫 Java 層靜態方法 FinalizerReference#add
InvokeWithJValues(soa, nullptr, WellKnownClasses::java_lang_ref_FinalizerReference_add, args);
*object = soa.Decode<mirror::Object>(arg.get());
}
FinalizerReference.java
```java // 關聯的引用佇列 public static final ReferenceQueue
實際物件暫存在 zombie 欄位中:
FinalizerReference.java
```java // 由虛擬機器維護,用於暫存實際物件 private T zombie;
// 2.2 取出引用所指向的物件(其實是取 zombie 欄位) @Override public T get() { return zombie; }
// 2.3 解除關聯關係,實際上虛擬機器內部早就解除關聯關係了,這裡只是返回暫存在 zombie 中的實際物件 @Override public void clear() { zombie = null; } ```
至此,Finalizer 機制實現原理分析完畢。
使用一張示意圖概括整個過程:
3. 總結
總結一下 Finalizer 機制最主要的環節:
- 1、為了實現物件的 Finalizer 機制,虛擬機器設計了
FinalizerReference
引用型別。重寫了Object#finalize()
方法的型別在類載入過程中會被標記位 finalizable 型別,每次建立物件時會同步建立關聯的 FinalizerReference 引用物件; - 2、不可達物件在即將被垃圾收集器回收時,虛擬機器會解除實際物件與引用物件的關聯關係,並將引用物件加入關聯的引用佇列中。然而,由於 finalizable 物件還需要執行 finalize() 方法,因此垃圾收集器會主動將物件標記為可達物件,並將實際物件暫存到 FinalizerReference 的
zombie
欄位中; - 3、守護執行緒
ReferenceQueueDaemon
會輪詢全域性臨時佇列unenqueued
佇列,將引用物件分別投遞到關聯的引用佇列中 - 4、守護執行緒
FinalizerDaemon
會輪詢觀察引用佇列,並執行實際物件上的 finalize() 方法。
參考資料
- Effective Java(第 3 版)(8. 避免使用 Finalizer 和 Cleanr 機制) —— [美] Joshua Bloch 著
- 深入理解 Android:Java 虛擬機器 ART(第 14 章 · ART 中的 GC) —— 鄧凡平 著
- 深入理解 Java 虛擬機器(第 3 版)(第 3 章 · 垃圾收集器與記憶體分配策略) —— 周志明 著
你的點贊對我意義重大!微信搜尋公眾號 [彭旭銳],希望大家可以一起討論技術,找到志同道合的朋友,我們下次見!
不只程式碼。
- Android IO 框架 Okio 的實現原理,到底哪裡 OK?
- 12 張圖看懂 CPU 快取一致性與 MESI 協議,真的一致嗎?
- Android 序列化框架 Gson 原理分析,可以優化嗎?
- 為什麼計算機中的負數要用補碼錶示?
- 什麼是二叉樹?
- 我把 CPU 三級快取的祕密,藏在這 8 張圖裡
- 全網最全的 ThreadLocal 原理詳細解析 —— 原理篇
- 程式設計師學習 CPU 有什麼用?
- WeakHashMap 和 HashMap 的區別是什麼,何時使用?
- 萬字 HashMap 詳解,基礎(優雅)永不過時 —— 原理篇
- Java 面試題:說一下 ArrayDeque 和 LinkedList 的區別?
- Java 面試題:說一下 ArrayList 和 LinkedList 的區別?
- Java 面試題:ArrayList 可以完全替代陣列嗎?
- 已經有 MESI 協議,為什麼還需要 volatile 關鍵字?
- JVM 系列(6)吊打面試官:為什麼 finalize() 方法只會執行一次?
- 使用字首和陣列解決"區間和查詢"問題
- NDK 系列(5):JNI 從入門到實踐,萬字爆肝詳解!
- 圖片系列(6)高低版本 Bitmap 記憶體分配與回收原理對比
- 如何使用並查集解決朋友圈問題?
- 為什麼你學不會遞迴?談談我的經驗