所有!
我在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有什么帮助?
为了更好地理解发生了什么,让我们看看执行代码后列表的样子。首先考虑一个初始列表:
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更快地采取行动。
也许有点晚了,但目前的解释对我来说完全不能令人满意,我想我有一个更明智的解释。
首先,每个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。
这是一个代码示例,说明了这个问题: http://pastebin.com/zTsLpUpq。
之后执行GC runWith()
并且对两个版本进行堆转储表示只有一个Item实例。