从字面上看,我不知道任何现有的 Map 或 Set 实现不会在添加和删除键时产生任何垃圾。
事实上,它甚至在技术上是可行的(在 Java 中,使用定义的Map
和Set
API)的唯一方法是,如果您要对条目数量设置严格的上限。实用的 Map 和 Set 实现需要与它们持有的元素数量成比例的额外状态。此状态必须存储在某处,并且当超出当前分配时,需要扩展存储。在 Java 中,这意味着需要分配新节点。
(好吧,你可以设计一个数据结构类来永远保留旧的无用节点,因此永远不会产生任何可收集的垃圾......但它仍然会产生垃圾。)
那么在实践中你能做些什么......以减少产生的垃圾量。我们HashMap
举个例子:
事实上,创建垃圾并不一定对性能不利。实际上,挂在节点上以使垃圾收集器不收集它们实际上可能会降低性能。
GC 运行的成本(假设是现代复制收集器)主要在三个方面:
- 寻找不是垃圾的节点。
- 将那些非垃圾节点复制到“to-space”。
- 更新其他非垃圾节点中的引用以指向“到空间”中的对象。
(如果您使用的是低暂停收集器,还有其他成本......通常与非垃圾量成正比。)
GC 工作中唯一真正取决于垃圾数量的部分是将垃圾对象曾经占用的内存归零以使其准备好重用。这可以通过bzero
对整个“从空间”的一次调用来完成......或使用虚拟内存技巧。
假设您的应用程序/数据结构挂在节点上以避免产生垃圾。现在,当 GC 运行时,它必须做额外的工作来遍历所有这些额外的节点,并将它们复制到“to-space”,即使它们不包含有用的信息。此外,这些节点正在使用内存,这意味着如果应用程序的其余部分产生垃圾,则存放垃圾的空间将减少,GC 将需要更频繁地运行。
如果您使用弱/软引用来允许 GC 从您的数据结构中收回节点,那么这对 GC 来说就更有用了……以及表示这些引用的空间。
注意:我并不是说对象池总是会使性能变差,只是它经常这样做,尤其是在池变得意外大的情况下。
当然,这就是为什么 HashMap 和类似的通用数据结构类不做任何对象池的原因。如果他们这样做了,他们会在程序员没有预料到的情况下表现得非常糟糕......而且他们真的会被打破,IMO。
最后,有一种简单的方法可以调整 HashMap,以便在删除相同键后立即添加添加不会产生垃圾(保证)。将其包装在一个 Map 类中,该类缓存“添加”的最后一个条目,并且仅在添加下一个条目时才put
真正HashMap
执行。当然,这不是通用解决方案,但它确实解决了您之前问题的用例。