关于java:JVM-系列6吊打面试官为什么-finalize-方法只会执行一次

40次阅读

共计 6905 个字符,预计需要花费 18 分钟才能阅读完成。

请点赞关注,你的反对对我意义重大。

🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,关注公众号 [彭旭锐] 带你建设外围竞争力。

前言

Java Finalizer 机制提供了一个在对象被回收之前开释占用资源的机会,然而都说 Finalizer 机制是不稳固且危险的,不举荐应用,这是为什么呢?明天咱们来深刻了解这个问题。

学习路线图:


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 有四大援用类型,除此之外,虚拟机外部还设计了 @hideFinalizerReference 类型来反对 Finalizer 机制。Reference 援用对象是用来实现更加灵便的对象生命周期治理而设计的对象包装类,Finalizer 机制也与对象的生命周期无关,因而存在这样“第 5 种援用类型”也能了解。

在虚拟机执行类加载的过程中,会将重写了 Object#finalize() 办法的类型标记为 finalizable 类型。每次在创立标记为 finalizable 的对象时,虚拟机外部会同时创立一个关联的 FinalizerReference 援用对象,并将其暂存到一个全局的链表中 (如果不存在全局的变量中,没有强援用持有的 FinalizerReference 自身在下次 GC 间接就被回收了)。

heap.cc

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

// 关联的援用队列
public static final ReferenceQueue<Object> queue = new ReferenceQueue<Object>();
// 全局链表头指针(应用一个双向链表持有 FinalizerReference,否则没有强援用的话援用对象自身间接就被回收了)private static FinalizerReference<?> head = null;

private FinalizerReference<?> prev;
private FinalizerReference<?> next;

// 从 Native 层调用
public static void add(Object referent) {
    // 创立 FinalizerReference 援用对象,并关联援用队列
    FinalizerReference<?> reference = new FinalizerReference<Object>(referent, queue);
    synchronized (LIST_LOCK) {
        // 头插法退出全局单链表
        reference.prev = null;
        reference.next = head;
        if (head != null) {head.prev = reference;}
        head = reference;
    }
}

public static void remove(FinalizerReference<?> reference) {// 从双向链表中移除,代码略}

2.3 在哪里执行 finalize() 办法?

依据咱们对援用队列的了解,当咱们在创立援用对象时关联援用队列,能够实现感知对象回收机会的作用。当援用指向的理论对象被垃圾回收后,援用对象会被退出援用队列。那么,是谁在生产这个援用队列呢?

在虚拟机启动时,会启动一系列守护线程,其中除了解决援用入队的 ReferenceQueueDaemon 线程,还包含执行 Finalizer 机制的 FinalizerDaemon 线程。FinalizerDaemon 线程会轮询察看援用队列,并执行理论对象上的 finalize() 办法。

提醒: FinalizerDaemon 是一个守护线程,因而 finalize() 的执行优先级低。

Daemons.java

public static void start() {
    // 启动四个守护线程
    ReferenceQueueDaemon.INSTANCE.start();
    FinalizerDaemon.INSTANCE.start();
    FinalizerWatchdogDaemon.INSTANCE.start();
    HeapTaskDaemon.INSTANCE.start();}

// 已简化
private static class FinalizerDaemon extends Daemon {private static final FinalizerDaemon INSTANCE = new FinalizerDaemon();

    // 这个队列就是 FinalizerReference 关联的援用队列
    private final ReferenceQueue<Object> queue = FinalizerReference.queue;

    FinalizerDaemon() {super("FinalizerDaemon");
    }

    @Override public void runInternal() {while (isRunning()) {
            // 1、从援用队列中取出援用
            FinalizerReference<?> finalizingReference = (FinalizerReference<?>)queue.poll();
            // 2、执行援用所指向对象 Object#finalize()
            doFinalize(finalizingReference);
            // 提醒:poll() 是非阻塞的,FinalizerDaemon 是与 FinalizerWatchDogDaemon 配合实现期待唤醒机制的}

    @FindBugsSuppressWarnings("FI_EXPLICIT_INVOCATION")
    private void doFinalize(FinalizerReference<?> reference) {
        // 2.1 移除 FinalizerReference 对象
        FinalizerReference.remove(reference);
        // 2.2 取出援用所指向的对象(不堪设想,为什么不为空呢?)Object object = reference.get();
        // 2.3 解除关联关系
        reference.clear();
        // 2.4 调用 Object#finalize()
        object.finalize();}
}

这里你有发现问题吗,当一般的援用对象在进入援用队列时,虚拟机曾经解除了援用对象与理论对象的关联,此时调用 Reference#get() 应该返回 null 才对。 但 FinalizerReference#get() 竟然还能拿到理论对象,理论对象不是曾经被回收了吗!? 这只能从源码中寻找答案。

2.4 FinalizerReference 援用对象入队过程

因为标记为 finalizable 的对象在被回收之前须要调用 finalize() 办法,因而这一类对象的回收过程比拟非凡,会经验两次 GC 过程。我将整个过程概括为 3 个阶段:

  • 阶段 1 – 首次 GC 过程: 当垃圾收集器发现对象变成不可达对象时,会解绑理论对象与援用对象的关联关系。当理论对象被革除后,会将援用对象退出关联的援用队列(这个局部咱们在上一篇文章中剖析过了)。然而,finalizable 对象还须要调用 finalize() 办法,所以首次 GC 时还不能回收理论对象。为此,垃圾收集器会被动将本来不可达的理论对象从新标记为可达对象,使其从本次垃圾收集中豁免,并且将理论对象长期保留到 FinalizerReference 的 zombie 字段中。理论对象与 FinalizerReference 的关联关系仍然会解除,否则会陷入死循环永远无奈回收;
  • 阶段 2 – FinalizerDaemon 执行 finalize() 办法: FinalizerDaemon 守护线程生产援用队列时,调用 ReferenceQueue#get() 只是返回暂存在 zombie 字段中的理论对象而已,其实此时关联关系早就解除了(这就是为什么 FinalizerReference#get() 还能够取得理论对象)。
  • 阶段 3 – 二次 GC: 因为理论对象和 FinalizerReference 曾经没有关联关系了,第二次回收过程跟一般对象雷同。前提是 finalize() 中将理论对象从新变成可达对象,那么二次 GC 不会那么快执行,要等到它从新变为不可达状态。

提醒: 这就是为什么 finalize() 办法只会执行一次,因为执行 finalize() 时理论对象和 FinalizerReference 曾经解除关联了,后续的垃圾回收跟一般的非 finalizable 对象一样。

源码摘要如下:

垃圾收集器清理过程:

办法调用链: ReclaimPhase→ProcessReferences→ReferenceProcessor::ProcessReferences→ReferenceQueue::EnqueueFinalizerReferences

reference_queue.cc

void ReferenceQueue::EnqueueFinalizerReferences(ReferenceQueue* cleared_references, collector::GarbageCollector* collector) {while (!IsEmpty()) {ObjPtr<mirror::FinalizerReference> ref = DequeuePendingReference()->AsFinalizerReference();
        mirror::HeapReference<mirror::Object>* referent_addr = ref->GetReferentReferenceAddr();
        // IsNullOrMarkedHeapReference:判断援用指向的理论对象是否被标记
        if (!collector->IsNullOrMarkedHeapReference(referent_addr, /*do_atomic_update*/false)) {
            // MarkObject:从新标记位可达对象
            ObjPtr<mirror::Object> forward_address = collector->MarkObject(referent_addr->AsMirrorPtr());
            // 将理论对象暂存到 zombie 字段
            ref->SetZombie<false>(forward_address);
            // 解除关联关系(一般援用对象亦有此操作)ref->ClearReferent<false>();
            // 将援用对象退出 cleared_references 队列(一般援用对象亦有此操作)cleared_references->EnqueueReference(ref);
        }
        DisableReadBarrierForReference(ref->AsReference());
    }
}

理论对象暂存在 zombie 字段中:

FinalizerReference.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 章 · 垃圾收集器与内存调配策略)—— 周志明 著

你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!

不只代码。

正文完
 0