OutOfMemoryError - 为什么一个等待的线程不能被垃圾回收?

5
这段简单的示例代码展示了一个问题。我创建了一个 ArrayBlockingQueue 和一个线程,使用 take() 等待从该队列中获取数据。在循环结束后,理论上队列和线程都可以被垃圾回收,但实际上很快就会出现 OutOfMemoryError 错误。是什么阻止了它们被GC,如何解决这个问题?
/**
 * Produces out of memory exception because the thread cannot be garbage
 * collected.
 */
@Test
public void checkLeak() {
    int count = 0;
    while (true) {

        // just a simple demo, not useful code.
        final ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<Integer>(2);
        final Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    abq.take();
                } catch (final InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();

        // perform a GC once in a while
        if (++count % 1000 == 0) {
            System.out.println("gc");
            // this should remove all the previously created queues and threads
            // but it does not
            System.gc();
        }
    }
}

我正在使用Java 1.6.0。

更新:在几个迭代之后执行垃圾回收,但这并没有帮助解决问题。

7个回答

8

线程是顶级对象。它们是“特殊的”,因此不遵循其他对象的相同规则。它们不依赖于引用来保持它们“活着”(即免受GC的影响)。只有线程结束后,它才会被垃圾回收。但是,在您的示例代码中,由于线程被阻塞,因此它不会结束。当然,既然线程对象没有被垃圾回收,那么由它引用的任何其他对象(在您的情况下是队列)也无法被垃圾回收。


5
您正在无限创建线程,因为它们都会阻塞,直到 ArrayBlockingQueue<Integer> abq 有一些条目。因此,最终您将获得一个 OutOfMemoryError(编辑) 每个线程都不会结束,因为它会阻塞直到 abq 队列有一个条目。 如果线程正在运行,则 GC 不会收集线程引用的任何对象,包括队列 abq 和线程本身。

是的,但当循环结束时,队列不再被引用。 - martinus
我的意思是,当循环的结尾再次从顶部开始时,先前创建的队列和线程不再被引用。 - martinus

2
abq.put(0);

这将会挽救你的一天。

你的所有线程都在等待其队列的take() 方法,但你从未往这些队列中放置任何东西。


0

System.gc 调用不起作用,因为没有要回收的内容。当线程启动时,它会增加线程的引用计数,如果不这样做,线程将不确定地终止。当线程的运行方法完成时,线程的引用计数将减少。

while (true) {
    // just a simple demo, not useful code.
    // 0 0 - the first number is thread reference count, the second is abq ref count
    final ArrayBlockingQueue<Integer> abq = new ArrayBlockingQueue<Integer>(2);
    // 0 1
    final Thread t = new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                abq.take();
                // 2 2
            } catch (final InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
    // 1 1
    t.start();
    // 2 2 (because the run calls abq.take)
    // after end of loop
    // 1 1 - each created object's reference count is decreased
}

现在,存在一个潜在的竞争条件 - 如果主循环在线程 t 有机会进行任何处理之前终止并进行垃圾回收,即在执行 abq.take 语句之前被操作系统挂起怎么办?运行方法将尝试在 GC 释放它之后访问 abq 对象,这是不好的。

为了避免竞争条件,应将对象作为参数传递给运行方法。我不确定 Java 这些天怎么样,已经过了一段时间了,所以我建议将对象作为构造函数参数传递给从 Runnable 派生的类。这样,在调用运行方法之前,就会多一个对 abq 的引用,从而确保对象始终有效。


Java不使用引用计数进行垃圾回收。 - TrayMan
快速搜索确认没有引用计数。将“引用计数”替换为“对象的引用数量”。只有当没有对对象的引用时,才会释放对象使用的内存。在示例代码中,存在一些引用,OP 没有意识到这些引用的存在。 - Skizz
即使使用引用计数,也可以通过让“t.start()”调用增加“t”的引用计数来轻松防止竞争条件。现在,当“t”超出范围时,仍然有一个引用计数,因此不会在“t”上发生GC。正如所指出的,Java不使用引用计数,但基本思想是相同的:线程在其“start()”方法被调用时标记为运行状态(因此被防止GC),而不仅仅是在新线程开始执行时。 - Christian Semrau

0

你启动了线程,因此所有这些新线程将在循环继续创建新线程的同时异步运行。

由于你的代码正在锁定,这些线程是系统中的生命引用,无法被收集。但即使它们正在执行一些工作,这些线程也不太可能像它们被创建那样快速终止(至少在这个示例中),因此GC无法收集所有内存,并最终会出现OutOfMemoryException。

创建尽可能多的线程既不高效也不明智。如果没有要求所有这些挂起的操作并行运行,您可以使用线程池和可运行项队列来处理。


1
@martinus 请仔细阅读。垃圾回收线程所需的时间比创建线程所需的时间更长。如果你在一个没有堵塞的浴缸里添加水,而且你加水的速度比排水口流出的速度快,那么浴缸最终会溢出。这是一种资源分配/释放竞态条件。如果只创建100个线程并继续执行while循环,会发生什么?这些线程最终会被回收吗? - Wedge
@wedge,不,它们永远不会被收集!或者做些其他的事情。即使我在每个循环末尾等待一秒钟,也没有任何东西被GC'd。 - martinus
3
这与创造与毁灭的时间无关。这些对象没有被收集,因为线程从未终止,因为它在等待一个永远不会填充的队列。(请参见其他帖子) - cadrian
1
@cadrian:没错,它会等待,但我的观点依然正确,他只是在发布之前创建它们。这才是关键,即使他添加了 put(0) 或其他内容来解除线程阻塞,他迟早也会陷入这种情况。 - Lucero
我猜有些人不喜欢我。我的回答已经被投票否决了5次,起初没有说明线程正在阻塞,但是对于导致OutOfMemoryException的问题是准确的。哦,好吧。 - Lucero
显示剩余4条评论

0

你的 while 循环是一个无限循环,并且它不断地创建新线程。虽然你在创建线程后立即启动了线程执行,但线程完成任务所需的时间大于创建线程所需的时间。

此外,在 while 循环内部声明 abq 参数的作用是什么?

根据你的编辑和其他评论。System.gc() 不能保证进行 GC 循环。请阅读我上面的陈述,你的线程执行速度低于创建速度。

我检查了 take() 方法的注释:“检索并删除此队列的头部,在此队列上没有元素时等待。”我看到你定义了 ArrayBlockingQueue,但你没有向其中添加任何元素,因此所有的线程都只是在等待该方法,这就是为什么你会遇到 OOM 的原因。


0

我不知道Java中线程是如何实现的,但有一个可能的原因让我想到为什么队列和线程不能被收集:这些线程可能是使用系统同步原语作为系统线程的包装器,在这种情况下,GC无法自动收集等待线程,因为它无法确定线程是否存活,即GC根本不知道线程不能被唤醒。

我无法确定修复它的最佳方法,因为我需要知道您要做什么,但您可以查看java.util.concurrent,看看它是否有适合您需求的类。


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