@[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 主导,流程如下:

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

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐