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 有什么帮助?
All!
I found strange code in 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;
}
Who can explain why do we need local variable h? How can it help for GC?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(4)
如果您查看 jsr166 src 然后你会发现有问题的提交,向下滚动到 v1.51
这表明答案就在这个 错误报告
完整的讨论可以在 jsr.166 邮件列表thread
“帮助 GC”是为了避免事情渗入终身。
If you look at the jsr166 src then you will find the offending commit, scroll down to v1.51
This shows the answer is in this bug report
The full discussion can be found in a jsr.166 mailing list thread
The "helping GC" bit is about avoiding things bleeding into tenured.
也许有点晚了,但目前的解释对我来说完全不满意,我想我有一个更合理的解释。
首先,每个 java GC 都会以某种方式从根集进行某种跟踪。这意味着,如果收集了旧头,我们无论如何都不会读取
next
变量 - 没有理由这样做。因此 IF head 在下一次迭代中被收集并不重要。上面这句话中的 IF 是这里的重要部分。设置在不同物体旁边的差异对于收集头本身并不重要,但可能会对其他物体产生影响。
让我们假设一个简单的分代GC:如果 head 位于 young set 中,那么无论如何它都会在下一次 GC 中被收集。但如果它在旧集合中,只有当我们执行完整的 GC 时才会收集它,而这种情况很少发生。
那么如果 head 位于旧集合中并且我们执行年轻 GC 会发生什么?在这种情况下,JVM 假定旧堆中的每个对象仍然存在,并将从旧对象到年轻对象的每个引用添加到年轻 GC 的根集中。这正是赋值所避免的:写入旧堆通常受到写屏障或其他东西的保护,以便 JVM 可以捕获此类赋值并正确处理它们 - 在我们的例子中,它会删除对象
next
指向根集,这确实会产生后果。简短的例子:
假设我们有
1 (old) -> 2(年轻)-> 3(xx)。如果我们现在从列表中删除 1 和 2,我们可能期望下一次 GC 会收集这两个元素。但是,如果仅发生年轻的 GC,并且我们没有删除旧的
next
指针,则元素 1 和 2 都不会被收集。与此相反,如果我们删除了 1 中的指针,2 将被年轻的 GC 收集。Maybe a bit late, but the current explanation is completely unsatisfactory to me and I think I've got a more sensible explanation.
First of all every java GC does some kind of tracing from a root set one way or another. This means that if the old head is collected we won't read the
next
variable anyhow - there's no reason to do so. Hence IF head is collected in the next iteration it doesn't matter.The IF in the above sentence is the important part here. The difference between setting next to something different doesn't matter for collecting head itself, but may make a difference for other objects.
Let's assume a simple generational GC: If head is in the young set, it will be collected in the next GC anyhow. But if it's in the old set it will only be collected when we do a full GC which happens rarely.
So what happens if head is in the old set and we do a young GC? In this case the JVM assumes that every object in the old heap is still alive and adds every reference from old to young objects to the root set for the young GC. And that's exactly what the assignment avoids here: Writing into the old heap is generally protected with a write barrier or something so that the JVM can catch such assignments and handle them correctly - in our case it removes the object
next
pointed to from the root set which does have consequences.Short example:
Assume we have
1 (old) -> 2 (young) -> 3 (xx)
. If we remove 1 and 2 now from our list, we may expect that both elements would be collected by the next GC. But if only a young GC occurs and we have NOT removed thenext
pointer in old, both elements 1 and 2 won't be collected. Contrary to this if we have removed the pointer in 1, 2 will be collected by the young GC..为了更好地理解发生的情况,让我们看看执行代码后列表的样子。首先考虑一个初始列表:
然后
h
指向head
和first
到h.next
:然后
h.next
指向h
且head
指向first
:现在,实际上我们知道只有一个活动引用指向第一个元素,它本身就是 (
h.next = h
),我们也知道 GC 收集不再有活动引用的对象,因此当方法结束时,列表的(旧)头可以被 GC 安全地收集,因为h< /code> 仅存在于该方法的范围内。
话虽如此,有人指出,并且我同意这一点,即使使用经典的出队方法(即只需使
first
指向head.next
和head
指向first
),不再有指向旧头的引用。然而,在这种情况下,旧的头在内存中悬空,并且其next
字段仍然指向first
,而在您发布的代码中,唯一剩下的是指向自身的孤立对象。这可能会触发 GC 更快地采取行动。To better understand what happens let's see how the list looks like after executing the code. First consider an initial list:
Then
h
points tohead
andfirst
toh.next
:Then
h.next
points toh
andhead
points tofirst
:Now, practically we know that there is only active reference pointing to the first element, which is by itself (
h.next = h
), and we also know that the GC collects objects that have no more active references, so when the method ends, the (old) head of the list ca be safely collected by the GC sinceh
exists only within the scope of the method.Having said this, it was pointed out, and I agree with this, that even with the classic dequeue method (i.e. just make
first
point tohead.next
andhead
point tofirst
) there are no more references pointing towards the old head. However, in this scenario, the old head is left dangling in memory and still has itsnext
field pointing towardsfirst
, whereas in the code you posted, the only thing left is an isolated object pointing to itself. This may be triggering the GC to act faster.下面的代码示例说明了该问题:http://pastebin.com/zTsLpUpq。
在
runWith()
之后执行 GC 并为两个版本进行堆转储表明只有一个 Item 实例。Here's a code sample that illustrates the question: http://pastebin.com/zTsLpUpq.
Performing a GC after
runWith()
and taking a heap dump for both versions says there's only one Item instance.