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 章 · 垃圾收集器与内存分配策略) —— 周志明 著
你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!
不只代码。
- LeetCode 周赛 336,多少人直接 CV?
- LeetCode 周赛 335,纯纯手速场!
- LeetCode 双周赛 98,脑筋急转弯转不过来!
- 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 从入门到实践,万字爆肝详解!