如果Java的finalize方法存在无限循环或死锁,Finalizer线程将会怎样处理?

16
如果在Java的finalize方法中出现无限循环或死锁情况,Finalizer线程会怎么处理?

4
你能否与我们分享你在尝试过程中观察到的任何行为? - Marko Topolnik
很难观察到这一点,JVM甚至不能保证finalize方法会被调用,更不用说你不知道它何时会被调用。 - morgano
@morgano 一个简单的 println 就足够了。我很久以前就玩过这个,finalize 很快就会被调用。只有在系统关闭时才会有一些无法访问的对象没有被终结的情况。 - Marko Topolnik
@MarkoTopolnik,这在你的情况下是这样的,但你不能确定其他JVM实现是否会表现相同。 - morgano
如果对象被垃圾回收了,finalize 方法将会被调用:"可以针对一个对象调用的 finalize 的特殊定义称为该对象的 finalizer。在垃圾回收器回收对象存储之前,Java虚拟机将会调用该对象的 finalizer。" - lpiepiora
1
@morgano 而且无法判断观察这个是否困难。具体来说,短暂的对象不会被继承,几乎可以保证很快就会被完成。 - Marko Topolnik
3个回答

17
规范写道:
在垃圾回收器回收对象的存储之前,Java虚拟机将调用该对象的finalizer。
Java编程语言未指定finalizer何时被调用,除了说它会在对象的存储被重用之前发生。
我理解为finalizer必须在存储可重用之前完成。
Java编程语言未指定哪个线程会调用给定对象的finalizer。
需要注意的是,许多finalizer线程可能处于活动状态(这在大型共享内存多处理器上有时是必需的),如果大型连接数据结构变成垃圾,则该数据结构中每个对象的finalize方法都可以同时调用,每个finalizer调用在不同的线程中运行。
也就是说,finalization可能发生在垃圾回收器线程、单独的线程或甚至是单独的线程池中。
JVM不允许简单地中止执行finalizer,并且只能使用有限数量的线程(线程是操作系统资源,操作系统不支持任意多个线程)。因此,非终止的finalizer必然会使该线程池饥饿,从而抑制任何可终止对象的收集并导致内存泄漏。
以下测试程序确认了这种行为:
public class Test {

    byte[] memoryHog = new byte[1024 * 1024];

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Finalizing " + this + " in thread " + Thread.currentThread());
        for (;;);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            new Test();
        }
    }
}

在Oracle JDK 7上,这将打印出:
Finalizing tools.Test@1f1fba0 in thread Thread[Finalizer,8,system]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
        at tools.Test.<init>(Test.java:5)
        at tools.Test.main(Test.java:15)

1
+1,做了差不多相同的测试,得到了相同的结果。只涉及一个终结器线程。 - Marko Topolnik
@mriton,**需要注意的是,在大型共享内存多处理器上可能会有许多终结器线程处于活动状态(这在某些情况下是必需的)**。这与我们选择的GC算法(如CMS)有任何关联吗?此外,终结器线程的数量和GC算法之间是否存在关系? - andy
1
一个JVM可以按照自己的方式执行此操作,不同的JVM可能会有不同的处理方式。如果您对特定的JVM感兴趣,请查阅其文档(或更可能是其源代码)。lpiepiora的回答似乎表明,在Oracle JVM中始终存在一个单独的finalizer线程,无论使用哪种垃圾收集算法。 - meriton
@meriton,那么我可以这样理解,终结器线程只是运行无法访问的对象的 finalize 方法,但是 GC 算法用于在 finalize 方法运行后回收无法访问的对象的内存。 - andy

4
Java规范没有告诉我们finalize方法应该如何被调用(只是必须在对象垃圾回收之前调用),因此行为是实现相关的。规范并不排除多个线程运行该过程,但也不要求它:重要的是要注意,许多终结器线程可能处于活动状态(这在大型共享内存多处理器上有时是必需的),并且如果一个大型连接数据结构成为垃圾,那么该数据结构中每个对象的finalize方法都可以同时调用,每个终结器调用在不同的线程中运行。查看JDK7的源代码,FinalizerThread保留计划进行完成的对象队列(实际上,当GC证明对象不可达时,将对象添加到队列中 - 请检查ReferenceQueue文档):
private static class FinalizerThread extends Thread {
    private volatile boolean running;
    FinalizerThread(ThreadGroup g) {
        super(g, "Finalizer");
    }
    public void run() {
        if (running)
            return;
        running = true;
        for (;;) {
            try {
                Finalizer f = (Finalizer)queue.remove();
                f.runFinalizer();
            } catch (InterruptedException x) {
                continue;
            }
        }
    }
}

每个对象都从队列中移除,然后运行runFinalizer方法。检查是否对该对象运行了终结器,如果没有,则调用一个本地方法invokeFinalizeMethod。该方法只是在对象上调用finalize方法:

JNIEXPORT void JNICALL
Java_java_lang_ref_Finalizer_invokeFinalizeMethod(JNIEnv *env, jclass clazz,
                                                  jobject ob)
{
    jclass cls;
    jmethodID mid;

    cls = (*env)->GetObjectClass(env, ob);
    if (cls == NULL) return;
    mid = (*env)->GetMethodID(env, cls, "finalize", "()V");
    if (mid == NULL) return;
    (*env)->CallVoidMethod(env, ob, mid);
}

这应该会导致对象排队在列表中,而FinalizerThread被阻塞在有故障的对象上,这反过来应该会导致OutOfMemoryError

因此,回答最初的问题:

如果Java finalize方法中存在无限循环或死锁,Finalizer线程会怎么做?

它将简单地坐在那里并运行该无限循环,直到OutOfMemoryError

public class FinalizeLoop {
    public static void main(String[] args) {
        Thread thread = new Thread() {
            @Override
            public void run() {
                for (;;) {
                    new FinalizeLoop();
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
        while (true);
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Finalize called");
        while (true);

    }
}

请注意,如果在JDK6和JDK7上只打印一次,则会打印“Finalize called”。

抱歉,我不同意你上面的示例代码。for循环将与主线程同时终止,因此你可能看不到任何东西 - andy
@andy 你说得对,我稍微改了一下代码,以确保最终你能看到一些东西 - 我想这可能取决于您的GC何时启动。谢谢! - lpiepiora

0

这些对象将不会被“释放”,也就是说它们的内存不会被回收,同时在 finalize 方法中释放的资源将会一直保留。

基本上,有一个队列保存着所有等待执行 finalize() 方法的对象。Finalizer 线程从该队列中取出对象,运行 finalize,然后释放该对象。

如果此线程出现死锁,则 ReferenceQueue 队列将增长,并且在某个时刻 OOM 错误将变得无法避免。此外,该队列中的对象会占用资源。希望这可以帮助你!

for(;;)
{
  Finalizer f = java.lang.ref.Finalizer.ReferenceQueue.remove();
  f.get().finalize();
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接