OutOfMemoryError - 为什么等待的Thread不能被垃圾收集?

时间:2009-04-24 08:55:17

标签: java multithreading garbage-collection out-of-memory

这个简单的示例代码演示了这个问题。我创建了一个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。

更新:在几次迭代后执行GC,但这没有帮助。

7 个答案:

答案 0 :(得分:8)

线程是顶级对象。它们是“特殊的”,因此它们不遵循与其他对象相同的规则。不依赖引用来保持它们“活着”(即从GC中安全)。线程在结束之前不会收集垃圾。由于线程被阻止,因此您的示例代码中不会发生这种情况。当然,既然线程对象没有被垃圾收集,那么它引用的任何其他对象(在你的情况下是队列)也不能被垃圾收集。

答案 1 :(得分:5)

您无限期地创建线程,因为它们都会阻塞,直到ArrayBlockingQueue<Integer> abq有一些条目。所以最终你会获得OutOfMemoryError

(适用编辑)

您创建的每个线程都将永远不会结束,因为它会阻塞abq队列作为一个条目。 如果线程正在运行,则GC不会收集线程引用的任何对象,包括队列abq和线程本身。

答案 2 :(得分:2)

abq.put(0);

应该节省你的一天。

你的线程都在队列的take()上等待,但你从不在这些队列中放置任何东西。

答案 3 :(得分:0)

启动线程,因此所有这些新线程将以异步方式运行,同时循环继续创建新线程。

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

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

答案 4 :(得分:0)

你的while循环是一个无限循环,它不断创建新的线程。虽然你一旦创建了线程执行就开始了,但是线程完成任务所花费的时间比创建线程的时间要长。

通过在while循环中声明它,使用abq参数做了什么?

根据您的修改和其他评论。 System.gc()不保证GC循环。阅读上面的语句,你的线程执行速度低于创建速度。

我检查了take()方法的注释“检索并删除此队列的头部,等待此队列中没有元素。”我看到你定义了ArrayBlockingQueue,但你没有添加任何元素,所以你的所有线程都在等待那个方法,这就是你获得OOM的原因。

答案 5 :(得分:0)

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

我不能说最好的修复方法是什么,因为我需要知道你要做什么,但是你可以看一下java.util.concurrent,看它是否有用于做你的事情的类需要。

答案 6 :(得分:0)

System.gc调用无效,因为无需收集任何内容。当线程启动时,它会增加线程引用计数,不这样做意味着线程将不确定地终止。当线程的run方法完成时,线程的引用计数就会递减。

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语句之前它被OS挂起,该怎么办?在GC释放之后,run方法将尝试访问abq对象,这将是不好的。

为了避免竞争条件,您应该将对象作为参数传递给run方法。我现在不确定Java,已经有一段时间了,所以我建议将对象作为构造函数参数传递给派生自Runnable的类。这样,在调用run方法之前有一个额外的abq引用,从而确保对象始终有效。