Java finalize() 方法
@[TOC](Java finalize() 方法)
Java finalize() 方法与 Finalizer 核心机制
Java 中 finalize() 方法的执行机制由 java.lang.ref.Finalizer 类、JVM 垃圾回收器(GC)及 FinalizerThread 线程协同实现:Finalizer 作为 JVM 引用体系的核心载体,内部封装了实际待回收的目标对象,连接 GC、finalize() 方法与 FinalizerThread;finalize() 是对象在 GC 回收前的回调方法,其执行完全依赖 GC 流程与 JVM 调度;FinalizerThread 则是负责执行 finalize() 方法的专属线程,三者共同构成了 finalize() 方法的完整执行链路。
核心组件的定义与关键特性
java.lang.ref.Finalizer 类
Finalizer 是 Java 核心类库中包私有(package-private) 的特殊类(仅对 java.lang.ref 包可见),继承自 java.lang.ref.Reference,是 JVM 引用体系的核心组成部分,其核心特性与定位如下:
- 访问约束:无 public 构造方法,开发者无法直接实例化、访问或继承,所有操作由 JVM 隐式完成;
- 核心使命:封装待执行 finalize() 方法的对象,作为 GC、finalize() 方法、FinalizerThread 三者间的 “数据载体” 与 “执行桥梁”——JVM 不直接处理待执行 finalize() 的对象,而是通过 Finalizer 实例间接管理;
- 内部结构:包含 next、prev 两个指针属性,作为实现链表结构的核心元素,分别指向队列中当前 Finalizer 实例的下一个元素和上一个元素,支撑其在链表容器中的管理;
- 生命周期管理:作为 Reference 子类,其实例生命周期由 JVM 引用处理器(ReferenceHandler)协同 GC 管控;
- 双容器维护:通过 unfinalized 静态链表(注册所有待执行 finalize() 的 Finalizer 实例,依赖 next/prev 维护链表结构)和 ReferenceQueue(待执行队列)双重容器维护状态,确保实例及封装的目标对象不被提前回收;
- 异常屏蔽:执行目标对象的 finalize() 方法时,捕获所有 Throwable 类型异常并静默忽略,不影响队列中其他 Finalizer 实例的处理。

finalize () 方法
finalize() 是 java.lang.Object 类定义的 protected 访问权限实例方法,所有 Java 对象通过继承机制天然拥有该方法,其核心属性如下:
- 绑定关系:执行逻辑仅与 GC 对不可达对象的处理流程强绑定,并非对象销毁的必选环节;
- 设计目标:为即将被 GC 回收的对象提供回调机会,用于释放非堆内存资源 —— 此类资源不在 JVM 堆管理范围内,GC 无法自动回收;
- 执行约束:JVM 不保证该方法必然执行,也不保证执行时机与多对象间的执行顺序;
- 执行主体:finalize() 方法由 FinalizerThread 线程专属处理,而非 GC 线程或应用主线程。
FinalizerThread 线程与待执行队列
FinalizerThread 是 JVM 内置的系统级守护线程,隶属于 java.lang.ref.Finalizer 类,其核心属性与职责如下:
- 线程属性:低优先级,与应用主线程、GC 线程异步执行;
- 核心职责:全权负责 finalize() 方法的调度与执行,持续轮询 Finalizer 专属的待执行队列,处理队列中的 Finalizer 实例;
- 待执行队列:FinalizerThread 的执行队列为 java.lang.ref.ReferenceQueue 类型的引用队列,内部采用链表结构实现(依托 Finalizer 的 next/prev 指针),是线程安全的无界队列;队列中的每一项均为 java.lang.ref.Finalizer 类型的引用对象(本质是一个引用),与虚引用(PhantomReference)、弱引用(WeakReference)同属 JVM 引用体系。

finalize () 方法的完整执行流程
finalize() 方法的执行仅触发于对象不可达、重写方法且未执行过该方法三重条件,全程由 JVM 主导,流程如下:
- 可达性分析与初步标记(触发前置)
JVM 垃圾回收器在执行回收周期时,以 GC Roots 为起点对堆内存中的对象执行可达性分析:- 若对象判定为可达(存在至少一条强引用链连接至 GC Roots):GC 不对其进行 finalize() 相关处理,对象保留在堆内存中;
- 若对象判定为不可达(无任何强引用链连接至 GC Roots):GC 进入对该对象的 finalize() 资格校验阶段。
- finalize () 执行资格校验(核心分支判定)
- 若该对象没有主动重写了 finalize() 方法或JVM 为该对象维护的 finalize () 执行状态标记为已执行,GC 直接将该对象标记为可回收,纳入本次 GC 回收候选集,等待内存释放;
- 若该对象主动重写了 finalize() 方法且JVM 为该对象维护的 finalize () 执行状态标记为未执行,那么 GC 将该对象的 finalize () 执行状态标记原子性置为待执行,并将该对象封装为 Finalizer 引用对象,移入 JVM 专属的 ReferenceQueue(Finalizer 待执行队列,内部为链表结构的线程安全无界队列),此时对象暂时脱离可回收状态。
- Finalizer 线程执行 finalize () 方法(异步执行环节)
每一个即将被回收且包含 finalize() 方法的对象,在正式回收前都会通过 Finalizer 实例加入 FinalizerThread 的执行队列;FinalizerThread 持续轮询该队列,当队列中存在待执行的 Finalizer 实例时,取出该实例并调用其内部方法执行目标对象的 finalize() 方法,执行过程遵循以下规则:- 异步性:FinalizerThread 的执行与 GC 线程、应用主线程相互独立,JVM 不保证 finalize() 方法的执行开始时间、执行时长,也不保证多个对象的执行顺序;
- 异常屏蔽:若 finalize() 执行过程中抛出任意 Throwable 类型异常,FinalizerThread 会捕获该异常并静默忽略,且不会中断队列中其他 Finalizer 实例的处理;
- 状态固化:finalize() 执行完成后,JVM 将该对象的 finalize () 执行状态标记原子性置为已执行,该标记永久不可逆转。
- 二次可达性分析与最终标记(回收判定环节)
finalize() 方法执行完成后,GC 对该对象重新执行可达性分析(核心目的是判定对象是否复活):- 对象未复活:若对象仍为不可达(未在 finalize() 中重建指向 GC Roots 的强引用链),GC 将该对象标记为可回收,纳入下一次 GC 回收候选集;
- 对象复活:若对象变为可达(在 finalize() 中通过赋值给静态变量 / 其他可达对象等方式重建强引用链),GC 撤销该对象的可回收标记,GC 撤销该对象的可回收标记,对象重新归为存活对象,本次 GC 不回收该对象;但该对象的 finalize () 执行状态标记已为已执行,若后续该对象再次变为不可达,GC 会直接跳过 finalize() 资格校验阶段,将其标记为可回收。
- 内存回收(最终执行环节)
针对标记为 “可回收” 的对象,JVM 按以下规则完成堆内存释放:- 无 finalize() 执行资格的对象:在本次 GC 周期中,JVM 释放其占用的堆内存空间,完成回收;
- 执行完 finalize() 仍不可达的对象:在下一次 GC 周期中,JVM 释放其占用的堆内存空间,完成回收;
- 复活后再次不可达且 finalize() 已执行的对象:在本次 GC 周期中,JVM 释放其占用的堆内存空间,完成回收。
- 对于复活的对象:若后续未再次变为不可达,则持续存活至应用终止或被显式置为不可达。

finalize () 执行的核心约束
JVM 对 finalize() 方法的执行存在严格约束,决定了其无法作为可靠的资源释放手段:
- 执行非必然性:JVM 不保证 finalize() 一定被执行(如进程异常终止、GC 未触发时,ReferenceQueue 待执行队列中的 Finalizer 实例可能未被处理);
- 时机非确定性:FinalizerThread 为低优先级线程,易被高优先级线程阻塞,导致 finalize() 执行存在不可控延迟;
- 顺序非关联性:多个对象的 finalize() 执行顺序与对象的创建顺序、入队顺序无必然对应关系;
- 执行唯一性:一个对象的 finalize() 仅能被 JVM 执行一次,finalize () 执行状态标记置为已执行后永久不可逆转。
在我前面的这篇博客里 Java虚拟机的垃圾对象判定 详细介绍了 finalize() 方法的核心缺陷以及推荐的释放资源方案。
示例代码
public class LongFinalize {
public static class LF {
private byte[] content = new byte[512];
/*@Override
protected void finalize() {
try {
System.out.println(Thread.currentThread().getId());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}*/
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
LF f = new LF();
}
long e = System.currentTimeMillis();
System.out.println(e - b);
}
}
-Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=“D:/analyzer/f.dump”

取消重写的 finalize() 方法注释,执行代码

结果发生了OOM错误并在 D:/analyzer/ 下得到了堆的Dump文件。
仔细阅读代码,不难发现,每次循环中产生的LF对象(占用大约512字节)都会在下一次循环中失效(因为局部变量作用域过期,对象也无其他引用),因此所有产生的LF对象都应该可以被回收。因此10MB堆空间,理论上应该完全可以满足需要,只是需要多进行几次GC而已,而这里为什么依然会出现OOM呢?
我们使用 MAT 工具打开得到的堆文件。

使用MAT的“Finalizer Overview” 功能可以更好地观察系统中的Finalizer


执行 OOM 的核心原因:
首先,Finalizer类的强引用绑定机制是内存无法释放的根本前提。由于 LF 类重写了finalize()方法,JVM 在创建每个 LF 对象时,都会隐式生成Finalizer实例来封装该 LF 对象,并将Finalizer实例加入unfinalized静态链表 —— 这条链表是强引用链,意味着即便 LF 对象在循环中失去局部引用,但因Finalizer的强引用持有,GC 始终无法回收这些 LF 对象,只有等FinalizerThread执行完该对象的finalize()方法,并将Finalizer实例从unfinalized链表移除后,LF 对象才具备被 GC 回收的条件。
其次,FinalizerThread的执行效率与对象创建速度形成极端失衡,是内存堆积的核心诱因。FinalizerThread本身是低优先级守护线程,而代码中finalize()方法包含Thread.sleep(1000),导致处理单个 LF 对象的finalize()方法至少需要 1 秒,执行效率低;但主线程却在毫秒级时间内循环创建 5 万个 LF 对象,对象创建速度远超FinalizerThread的处理速度,大量Finalizer实例(及封装的 LF 对象)持续堆积在unfinalized链表中,无法进入后续的回收流程。
最后,堆内存上限的约束直接触发了 OOM。每个 LF 对象约占用 512 字节,5 万个 LF 对象理论占用约 24.4MB,远超 JVM 配置的 10MB 堆内存上限。这些被Finalizer强引用持有的 LF 对象持续占用堆内存,且无法被 GC 释放,堆内存快速被占满,最终触发OutOfMemoryError;而 Dump 文件中出现大量Finalizer类,也印证了FinalizerThread执行队列堆积、对象长期被引用无法释放的核心问题。
更多推荐



所有评论(0)