5

这个简单的示例代码演示了这个问题。我创建了一个ArrayBlockingQueue, 和一个线程,该线程使用take(). 循环结束后,理论上队列和线程都可以被垃圾回收,但实际上我很快就会得到一个OutOfMemoryError. 是什么阻止了这被 GC'd,以及如何解决这个问题?

/**
 * 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,但这无济于事。

4

7 回答 7

8

Threads are top level objects. They are 'special' so they do not follow the same rules as other objects. The do not rely on references to keep them 'alive' (i.e. safe from GC). A thread will not get garbage collected until it has ended. Which doesn't happen in your sample code, since the thread is blocked. Of course, now that the thread object is not garbage collected, then any other object referenced by it (the queue in your case) also cannot be garbage collected.

于 2009-04-24T12:57:02.503 回答
5

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

(编辑)

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

于 2009-04-24T09:08:07.983 回答
2
abq.put(0);

应该可以节省您的时间。

你的线程都在他们的队列上等待,take()但你从来没有在这些队列中放任何东西。

于 2009-04-24T09:20:50.120 回答
0

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

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

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

于 2009-04-24T09:03:03.407 回答
0

您的 while 循环是一个无限循环,它会不断创建新线程。尽管您在创建线程后立即开始执行线程,但是线程完成任务所花费的时间大于创建线程所花费的时间。

通过在while循环中声明abq参数还有什么作用?

根据您的编辑和其他评论。System.gc() 不保证 GC 周期。阅读我上面的语句,您的线程的执行速度低于创建速度。

我检查了 take() 方法的注释“检索并删除此队列的头部,如果此队列上不存在任何元素,则等待。” 我看到您定义了 ArrayBlockingQueue 但您没有向其中添加任何元素,因此您的所有线程都只是在等待该方法,这就是您获得 OOM 的原因。

于 2009-04-24T09:09:20.663 回答
0

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

我不能说修复它的最佳方法是什么,因为我需要知道您要做什么,但是您可以查看 java.util.concurrent 以查看它是否具有执行所需操作的类。

于 2009-04-24T09:38:55.273 回答
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 语句之前被操作系统挂起怎么办?run 方法会在 GC 释放后尝试访问 abq 对象,这很糟糕。

To avoid the race condition, you should pass the object as a parameter to the run method. I'm not sure about Java these days, it's been a while, so I'd suggest passing the object as a constructor parameter to a class derived from Runnable. That way, there's an extra reference to abq made before the run method is called, thus ensuring the object is always valid.

于 2009-04-24T09:44:06.663 回答