为什么这个线程池没有被垃圾回收?

39
在这个代码示例中,ExecutorService 被使用了一次,然后允许其超出作用域范围。
public static void main(String[] args)
{
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    executorService.submit(new Runnable()
    {
        public void run()
        {
            System.out.println("hello");
        }
    });
}

一旦executorService超出作用域,它应该被收集和终止。ThreadPoolExecutor中的finalize()方法调用shutdown()。

/**
 * Invokes {@code shutdown} when this executor is no longer
 * referenced and it has no threads.
 */
protected void finalize() {
    shutdown();
}

调用shutdown()后,线程池应该终止并允许JVM退出。但是executorSerivce从未被回收,因此JVM仍然存活。即使调用System.gc()也无效。为什么即使main()终止后executorService仍未被回收?

注意:我知道我应该自己调用shutdown(),在测试之外我总是这样做。我想知道为什么在这里作为备份的finalization没起作用。


3
因为垃圾回收是“非确定性”的,也就是说你无法预测它何时发生,因此你也无法准确预测 finalize 方法将在何时运行。你只能使对象变得可回收,并使用 System.gc() 建议进行垃圾回收,但并不能保证。 - jpse
你不应该依赖 finalization 来关闭或终止某个东西。正如 jpse 所说,垃圾回收是非确定性的,而 finalization 不能保证永远运行。 - Thomas
1
JVM没有退出,因为工作线程在workQueue.take()中被阻塞。 - Ovidiu Lupas
6个回答

59

这与垃圾回收器是不确定性的没有真正关系,虽然这并没有帮助!(在您的示例中,这是一个原因,但即使我们“修复”它以消耗内存并强制进行收集,它仍然不会终止)

执行器创建的工作线程是内部类,它们具有对执行器本身的引用。(它们需要这个引用才能看到队列、运行状态等!)正在运行的线程不会被垃圾收集,因此当池中的每个线程都具有该引用时,它们将保持执行器处于活动状态,直到所有线程死亡。如果您不手动停止线程,它们将永远运行,而您的JVM将永远不会关闭。


线程池中的线程与线程池之间的引用在哪里?我认为可能是这种情况,到处都找了一遍,没有找到任何引用。 - Craig P. Motlin
1
Worker是一个内部类,编译器会自动添加它。 - Affe
1
你应该在主要答案中添加有关Worker的信息,我认为这是我们都缺少的关键细节。 - Craig P. Motlin

23

Affe说得对;线程池中的线程会防止它被垃圾回收。当您调用Executors.newFixedThreadPool(3)时,您会得到一个像这样构建的ThreadPoolExecutor:

ThreadPoolExecutor(3, 3, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

如果你阅读ThreadPoolExecutor的JavaDoc,它会说:

一个池在程序中不再被引用且没有剩余线程将自动关闭。如果您想确保即使用户忘记调用shutdown(),未引用的池也会被回收,那么您必须安排未使用的线程最终死亡,通过设置适当的保持活动时间,使用零核心线程的下限和/或设置allowCoreThreadTimeOut(boolean)。

如果您希望您的线程池像您期望的那样完成,您应该执行其中之一。


我仍然不明白我需要做什么,如何设置保持连接时间? - troy

1

终结器太不可预测了,依赖它们通常是不好的实践。

您可以在 Joshua Bloch 的《Effective Java》(1.7条款)中了解更多信息"(链接)"


1
链接已过期。 - Stefan Bollmann

1
如果您希望在 Executor 服务超出范围时终止线程,应避免使用 mjt 建议的方式。
    ExecutorService executorService = Executors.newFixedThreadPool(3);`

并且例如使用:

ExecutorService executorService = new ThreadPoolExecutor(0, 3, 10, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

0

因为垃圾回收是“非确定性”的,也就是说你无法预测它何时发生,因此你也无法准确预测 finalize 方法将在何时运行。你只能使对象有资格进行垃圾回收,并使用 System.gc() 建议进行垃圾回收,但并不保证一定会执行。

更糟糕的是,线程是由 JVM 处理的操作系统特定的,几乎是不可预测的...


就是这么简单;这就是GC的定义 :) - KevinDTimm
1
你可以通过创建一堆垃圾来强制运行GC,但它仍然不会被收集,因此看起来Affe的答案是正确的原因。 - Craig P. Motlin

0
一旦executorService超出范围,它应该被收集和完成。
实际上并非如此-一旦超出范围,它可能被收集和完成。在VM规范中没有关于何时对象被完成甚至是否被完成的保证:

Java编程语言未指定多快会调用终结器,除了说它会在对象的存储被重用之前发生。


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