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

所有!

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

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

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

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

 1 -> 2 -> 3 

然后h指向headfirst指向h.next

 1 -> 2 -> 3 | | h first 

然后h.next指向h并指向first

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

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

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

如果你查看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”一点是为了避免事情流失到终身。

干杯

马特

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

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

上述句子中的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。

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