清空集合(在我的情况下是一个 ArrayList)与创建新集合(并让垃圾收集器清除旧集合)的优缺点是什么。
具体来说,我有一个ArrayList<Rectangle>
叫list
. 当某种情况发生时,我需要清空list
并用其他内容重新填充它。我应该打电话list.clear()
还是只做一个新的ArrayList<Rectangle>
,让旧的被垃圾收集?每种方法的优缺点是什么?
清空集合(在我的情况下是一个 ArrayList)与创建新集合(并让垃圾收集器清除旧集合)的优缺点是什么。
具体来说,我有一个ArrayList<Rectangle>
叫list
. 当某种情况发生时,我需要清空list
并用其他内容重新填充它。我应该打电话list.clear()
还是只做一个新的ArrayList<Rectangle>
,让旧的被垃圾收集?每种方法的优缺点是什么?
回收一个ArrayList
(例如通过调用clear
)的优点是可以避免分配一个新的开销,以及增加它的成本......如果你没有提供一个好的initialCapacity
提示。
回收的缺点ArrayList
包括:
该clear()
方法必须分配给s 后备数组null
中的每个(已使用)插槽。ArrayList
clear()
不会调整后备数组的大小以释放内存。因此,如果您反复填充和清除列表,它将最终(永久)使用足够的内存来表示它遇到的最大列表。换句话说,您增加了内存占用。您可以通过调用来解决这个问题trimToSize()
,但这会创建一个垃圾对象,等等1。
存在可能影响性能的地方性和跨代问题。当您反复回收 一个ArrayList
时,该对象及其后备数组可能会被永久使用。这意味着:
列表对象和表示列表元素的对象可能位于堆的不同区域,可能会增加 TLB 未命中和页面流量,尤其是在 GC 时。
将(年轻一代)引用分配到(终身)列表的后备数组中可能会产生写屏障开销……取决于 GC 实现。
不可能准确地模拟现实应用程序的性能权衡。变量太多了。然而,“公认的智慧”是,如果你有足够的内存2和一个半体面的垃圾收集器,回收通常不是一个好主意。
还值得注意的是,现代 JVM 可以非常有效地分配对象。它只需要更新堆的“空闲”指针并写入 2 或 3 个对象头字。内存归零是由 GC 完成的……除此之外,这样做的工作大致相当于将clear()
正在回收的列表中的引用归零的工作。
1 - 创建一个新的 ArrayList 比调用 clear() 后跟 trimToSize(...) 更好。使用后者,您将获得垃圾收集开销和多余归零的开销。
2 - 如果垃圾对象与非垃圾对象的比例很高,则复制收集器效率更高。如果分析这种收集器的工作方式,成本几乎都发生在查找和复制可达对象上。对垃圾对象唯一需要做的就是将撤离的“来自”空间的块零写入准备好分配新对象。
我的建议是不要回收ArrayList
对象,除非你有明显的需要最小化(垃圾)对象的创建率;例如,因为它是减少(有害)GC 暂停的唯一选择。
在所有条件相同的情况下,在现代 Hotspot JVM 上,我的理解是通过执行以下操作可以获得最佳性能:
initialSize
分配列表对象时使用准确的提示。稍微高估总比低估好。您保留容器并clear
在您想减少 GC 负载时调用:clear()
将数组内的所有引用归零,但不会使数组符合垃圾收集器回收的条件。这可能会加快未来的插入,因为里面的数组ArrayList
不需要增长。当您计划添加到容器的数据与您清除的数据大小大致相同时,这种方法特别有利。
此外,您可能需要clear
在其他对象持有对您将要清除的数组的引用时使用。
当新数据的大小可能与以前不同时,释放容器并创建一个新容器是有意义的。当然你可以通过 clear()
结合调用来达到类似的效果trimToSize()
。
由于已经写了有趣的点,您可以更深入地考虑它。
阅读有关中断模式的文章后,我还没有意识到这一点,请参阅LMAX 的中断模式如何工作?
不仅可以重用底层集合,还可以重用集合中的实体。
例如,假设生产者和消费者用例。生产者可以一遍又一遍地将数据填充到相同的(循环)数组中,甚至使用相同的实体。只需清除属性、内部状态并填充它自己的。
从 GC 的角度来看,这是一个更好的解决方案。但这显然是特殊情况,并非对每个问题都有用。
真的没关系...
List.clear() 实现将内部数组的引用设置为 null。如果没有更多引用,则有效地将对象设置为垃圾回收。
如果您唯一关心的是内存,那么这两种方法都没有真正可衡量的差异。即使在操作方面,差异也将在于数组分配(在调整大小操作中)和其他此类操作。
但是清除它可能会稍微好一些,尽管如果创建一个新列表更具可读性,我会这样做。