这个简单的示例代码演示了这个问题。我创建了一个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,但这没有帮助。
答案 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引用,从而确保对象始终有效。