问题 java.util.concurrent.LinkedBlockingQueue中的奇怪代码


所有!

我在LinkedBlockingQueue中发现了奇怪的代码:

private E dequeue() {
        // assert takeLock.isHeldByCurrentThread();
        Node<E> h = head;
        Node<E> first = h.next;
        h.next = h; // help GC
        head = first;
        E x = first.item;
        first.item = null;
        return x;
}

谁能解释为什么我们需要局部变量h?它对GC有什么帮助?


5070
2018-01-11 12:34


起源

也许评论已经过时了(你能找到版本控制并追踪提交的评论吗?):) - milan
有趣的是,这个临时变量和注释仅在java 7中添加。所以它最终是出于某种意图。看到 grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/... 和 grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/... - Vadzim
这是openjdk,Mac上的Oracle jdk 6中的LinkedBlockingQueue(1.6.0_29-b11-402.jdk)有临时变量。 - milan
至少它肯定不存在于java 5中。 - Vadzim


答案:


为了更好地理解发生了什么,让我们看看执行代码后列表的样子。首先考虑一个初始列表:

1 -> 2 -> 3

然后 h 指着 head 和 first 至 h.next

1 -> 2 -> 3
|    |
h    first

然后 h.next 指着 h 和 head 指着 first

1 -> 2 -> 3
|   / \
h head first

现在,实际上我们知道只有活动引用指向第一个元素,它本身就是(h.next = h),我们也知道GC收集没有更多活动引用的对象,因此当方法结束时,GC的安全收集列表的(旧)头部 h 仅存在于方法的范围内。

话虽如此,有人指出,我同意这一点,即使采用经典的出列方法(即只是制造 first 指向 head.next 和 head 指向 first)没有更多的引用指向旧头。然而,在这种情况下,旧的头部留在内存中仍然有它的 next 指向的领域 first,而在您发布的代码中,唯一剩下的就是指向自身的孤立对象。这可能会触发GC更快地采取行动。


1
2018-01-11 12:51



无论如何,即使没有,也没有更多的参考资料 h.next = h。 - Vadzim
@Vadzim:我没有说明,我只是在解释他发布的代码中发生了什么。 - Tudor
问题不在于此。这是为什么不仅仅是 first = head = head.next。但无论如何,谢谢图形。请注意,head始终不包含第一个项目。 - Vadzim
为什么引用可达对象可能会打扰GC? - Vadzim
@Vadzim:理论上它没有,但在我看到GC的实际实现之前,我想我可以假设任何微妙的差异都可能影响它决定收集和反对的速度。 - Tudor


如果你查看jsr166 src,你会发现违规提交

http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/main/java/util/concurrent/LinkedBlockingQueue.java?view=log (见v 1.51)

这表明答案在这个错误报告中

http://bugs.sun.com/view_bug.do?bug_id=6805775

完整的讨论在这个主题中

http://thread.gmane.org/gmane.comp.java.jsr.166-concurrency/5758

“帮助GC”一点是为了避免事情流失到终身。

干杯

马特


6
2018-01-15 10:21





也许有点晚了,但目前的解释对我来说完全不能令人满意,我想我有一个更明智的解释。

首先,每个java GC都会以某种方式从根集中进行某种跟踪。这意味着如果收集旧头,我们将不会阅读 next 变量无论如何 - 没有理由这样做。于是 如果 在下一次迭代中收集头并不重要。

上述句子中的IF是这里的重要部分。设置旁边不同的东西之间的区别对于收集头本身无关紧要,但可能对其他对象产生影响。

让我们假设一个简单的世代GC:如果头部在年轻的集合中,无论如何它将被收集在下一个GC中。但如果它在旧的集合中,那么只有当我们完成很少发生的完整GC时才会收集它。

那么如果head在旧版本中并且我们做了一个年轻的GC会发生什么呢?在这种情况下,JVM假定旧堆中的每个对象仍然存活,并将从旧对象到年轻对象的每个引用添加到年轻GC的根集。这正是赋值在这里避免的:写入旧堆通常受写入障碍或其他东西的保护,以便JVM可以捕获这样的赋值并正确处理它们 - 在我们的例子中它删除了对象 next从根集指出,确实有后果。

简短的例子:

假设我们有 1 (old) -> 2 (young) -> 3 (xx)。如果我们现在从列表中删除1和2,我们可能会期望下一个GC将收集这两个元素。但是,如果只有一个年轻的GC发生,我们没有删除 next 指针在旧时,元素1和2都不会被收集。与此相反,如果我们在1中删除指针,则年轻的GC将收集2。


4
2018-01-14 03:44



我真的没有看到你说的和我说的有什么区别。我还说过,不移除下一个可能会导致GC更快地收集头部。 - Tudor
@Tudor嗯,我解释了哪种机制会导致行为的改变以及在什么情况下会发生这种情况,这对我来说肯定比“有些东西可能导致GC采取不同行为”更为重要。此外,它不是关于先前收集的头部(因为这显然是无意义的),而是关于后续元素 - 这些是不同的对象。 - Voo
我在评论中已经说过,在没有实际实现的情况下,我只能推测该任务应该做什么。另一方面,你做了很多绝对的陈述而没有实际提供任何来源和使用的术语,如“年轻的GC”,而没有实际介绍它们。 - Tudor
@Tudor是的我正在假设一些关于Java垃圾收集器(实际上是任何代际GC)如何工作的基本知识。在一个关于GC的一些微调优化的问题似乎并非不合理 - 否则任何关于GC细节的帖子都会让页面长。是否有限制假设一代GC?在某种程度上肯定,但我所知道的每一个生产GC(azul,热点和ibm中常见的那些 - 对于.NET也是如此),出于显而易见的原因,它们以某种方式存在世代相传的概念。 - Voo


这是一个代码示例,说明了这个问题: http://pastebin.com/zTsLpUpq。 之后执行GC runWith() 并且对两个版本进行堆转储表示只有一个Item实例。


0
2018-01-11 15:37



那么GC的“帮助”是什么? - Vadzim
也许在某些情况下,我不知道,也许我们可以找到写评论的人:) - milan