垃圾收集器与集合

Garbage collector vs. collections

我在 Java 中阅读了一些关于垃圾收集的帖子,但我仍然无法确定明确清除收集是否被认为是一种好的做法...并且由于我找不到明确的答案,我决定在这里问一下。

考虑这个例子:

List<String> list = new LinkedList<>();
// here we use the list, perhaps adding hundreds of items in it...
// ...and now the work is done, the list is not needed anymore
list.clear();
list = null;

从我在例如实现中看到的LinkedListHashSetclear() 方法基本上只是循环给定集合中的所有项目,设置其所有元素(在 LinkedList 的情况下还引用下一个和上一个元素) 至 null

如果我做对了,将 list 设置为 null 只会从 list 中删除一个引用 - 考虑到它是对它的唯一引用,垃圾收集器最终会采取照顾它。我只是不知道在这种情况下,垃圾收集器还需要多长时间才能处理列表的元素。

所以我的问题是 - 上面列出的示例代码的最后两行实际上是否有助于垃圾收集器更有效地工作(即更早地收集列表的元素)或者我会让我的应用程序忙于 "irrelevant tasks"?

我认为 clear() 在这种情况下不会有帮助。一旦不再有对项目的引用,GC 将删除项目,因此理论上,只需设置 list = null 将具有相同的效果。 您无法控制何时调用 GC,因此在我看来,除非您有特定的 resource/performance 要求,否则不值得担心。就我个人而言,我仍然会使用 list = null;

如果你想重用列表变量,那么 clear() 当然是最好的选择,而不是创建一个新的列表对象。

最后两行没有帮助。

  • 一旦 list 变量超出范围 *,如果这是对链表的最后引用,则该列表符合垃圾条件collection。立即将 list 设置为 null 不会增加任何值。

  • 一旦列表符合垃圾条件collection,那么如果列表包含对它们的唯一引用,则对其元素进行处理。不需要清除列表。

在大多数情况下,您可以信任垃圾收集器来完成它的工作,而不需要 "help" 它。

*迂腐地说,控制垃圾的不是scopecollection,而是可达性。可达性不容易用一句话概括。有关此区别的解释,请参阅 this Q&A


此规则的一个常见例外是,如果您的代码保留引用的时间比需要的时间长。典型的例子是听众。如果您向某个组件添加了一个侦听器,并且稍后不再需要该侦听器,则需要显式将其删除。如果不这样做,该侦听器可以抑制其自身和它引用的 objects 的垃圾 collection。

假设我像这样向按钮添加了一个侦听器:

button.addListener(event -> label.setText("clicked!"));

后来标签被移除,但按钮仍然存在。

window.removeChild(label);

这是一个问题,因为按钮引用了侦听器,而侦听器引用了标签。即使标签在屏幕上不再可见,也无法对其进行垃圾回收。

现在是采取行动并站在 GC 一边的时候了。添加时需要记住监听器...

Listener listener = event -> label.setText("clicked!");
button.addListener(listener);

...这样我就可以在完成标签后将其删除:

window.removeChild(label);
button.removeListener(listener);

在 Java 中,对象要么处于活动状态(可通过其他对象拥有的引用访问),要么处于死状态(任何其他对象的引用所有者无法访问)。只能从死对象到达的对象也被认为是死的并且有资格进行垃圾收集。

如果没有活动对象引用您的集合,则它不可访问且符合垃圾回收条件。这也意味着您的集合的所有元素(以及它可能创建的任何其他辅助对象)无法访问,除非其他一些活动对象引用了它们。

因此,除了清除一个死对象对另一个对象的引用外,clear 方法没有任何作用。他们将以任何一种方式收集垃圾。

这取决于以下因素

  • 如何实现clear()
  • 集合中条目的分配模式
  • 垃圾收集器
  • 是否有其他东西保存在集合或它的子视图上(不适用于您的示例,但在现实世界中很常见)

对于原始的、非分代的跟踪垃圾收集器,清除引用仅意味着额外的工作,而不会使 GC 上的事情变得更容易。但如果您不能保证及时清除对集合的所有引用,清除可能仍然有帮助。

对于分代 GC,尤其是 G1GC 清空集合(或引用数组)内的引用,可能某些情况下有帮助减少跨区域引用。

但这只有在您实际拥有在不同区域创建对象并将它们放入位于另一个区域的集合中的分配模式时才有用。而且它还依赖于 clear() 实现清空这些引用,这将清除变成 O(n) 操作,而它通常可以作为 实现O(1)一个.

因此,对于您的具体示例,答案如下:

如果

  • 你的列表是长期存在的
  • 在该代码路径上创建的列表使 up/hold 成为应用程序产生的垃圾的很大一部分
  • 您正在使用 G1 或类似的多代收集器
  • 在最终释放之前慢慢积累对象(这通常将它们放在不同的区域,从而创建跨区域引用)
  • 您希望用 CPU 的清理时间来减少 GC 工作量
  • clear() 实现是 O(n) 而不是 O(1),即清空所有条目。 OpenJDK 的 1.8 LinkedList 就是这样做的。

那么在释放集合本身之前调用 clear() 可能可能 有益。

因此,充其量这是一个非常特定于工作负载的微优化,仅应在 profiling/monitoring 实际条件下的应用程序并确定 GC 开销证明额外的清理成本合理之后应用。


供参考,OpenJDK 1.8 的 LinkedList::clear

/**
 * Removes all of the elements from this list.
 * The list will be empty after this call returns.
 */
public void clear() {
    // Clearing all of the links between nodes is "unnecessary", but:
    // - helps a generational GC if the discarded nodes inhabit
    //   more than one generation
    // - is sure to free memory even if there is a reachable Iterator
    for (Node<E> x = first; x != null; ) {
        Node<E> next = x.next;
        x.item = null;
        x.next = null;
        x.prev = null;
        x = next;
    }
    first = last = null;
    size = 0;
    modCount++;
}