ChatGPT解决这个技术问题 Extra ChatGPT

创建数百万个小型临时对象的最佳实践

创建(和发布)数百万个小对象的“最佳实践”是什么?

我正在用 Java 编写一个国际象棋程序,搜索算法为每个可能的移动生成一个“移动”对象,名义搜索每秒可以轻松生成超过一百万个移动对象。 JVM GC 已经能够处理我的开发系统上的负载,但我有兴趣探索以下替代方法:

最大限度地减少垃圾收集的开销,并减少低端系统的峰值内存占用。

绝大多数对象的生命周期都很短,但大约 1% 生成的移动被持久化并作为持久值返回,因此任何池化或缓存技术都必须提供排除特定对象重复使用的能力.

我不期望完整的示例代码,但我会很感激进一步阅读/研究的建议,或类似性质的开源示例。

享元模式是否适合您的情况? en.wikipedia.org/wiki/Flyweight_pattern
你需要将它封装在一个对象中吗?
享元模式不合适,因为对象不共享重要的公共数据。至于将数据封装在一个对象中,它太大而无法打包成一个原语,这就是为什么我正在寻找 POJO 的替代品。

P
Peter Mortensen

使用详细垃圾收集运行应用程序:

java -verbose:gc

它会告诉你什么时候收集。将有两种类型的扫描,快速扫描和完全扫描。

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

箭头是大小之前和之后。

只要它只是进行 GC 而不是完整的 GC,您就可以安全回家。常规 GC 是“年轻一代”中的副本收集器,因此不再引用的对象只是被遗忘了,这正是您想要的。

阅读 Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning 可能会有所帮助。


尝试使用 Java 堆大小来尝试找到一个完全垃圾收集很少见的点。在 Java 7 中,新的 G1 GC 在某些情况下更快(而在其他情况下更慢)。
P
Peter Mortensen

从版本 6 开始,JVM 的服务器模式采用了 escape analysis 技术。使用它,您可以一起避免 GC。


逃逸分析常常令人失望,值得检查 JVM 是否已经弄清楚你在做什么。
如果您有使用这些选项的经验:-XX:+PrintEscapeAnalysis 和 -XX:+PrintEliminateAllocations。那将是伟大的分享。因为我没有,老实说。
请参阅stackoverflow.com/questions/9032519/…,您将需要为 JDK 7 获取调试版本,我承认我没有这样做,但使用 JDK 6 已经成功。
P
Pierre Laporte

好吧,这里有几个问题!

1 - 如何管理短期对象?

如前所述,JVM 可以完美地处理大量的短期对象,因为它遵循 Weak Generational Hypothesis

请注意,我们说的是到达主内存(堆)的对象。这并非总是如此。您创建的许多对象甚至没有留下 CPU 寄存器。例如,考虑这个 for 循环

for(int i=0, i<max, i++) {
  // stuff that implies i
}

让我们不要考虑循环展开(JVM 在您的代码上大量执行的优化)。如果 max 等于 Integer.MAX_VALUE,则循环可能需要一些时间来执行。但是,i 变量永远不会逃脱循环块。因此 JVM 会将该变量放入 CPU 寄存器中,定期递增它,但永远不会将其发送回主存。

因此,如果仅在本地使用它们,创建数百万个对象并不是什么大问题。它们在存储在 Eden 之前就已经死亡,因此 GC 甚至不会注意到它们。

2 - 减少 GC 的开销有用吗?

像往常一样,这取决于。

首先,您应该启用 GC 日志记录以清楚地了解正在发生的事情。您可以使用 -Xloggc:gc.log -XX:+PrintGCDetails 启用它。

如果您的应用程序在 GC 周期中花费大量时间,那么,是的,调整 GC,否则,它可能不值得。

例如,如果您每 100 毫秒有一个年轻 GC,需要 10 毫秒,那么您将 10% 的时间花在 GC 上,并且每秒有 10 次收集(这是 huuuuuge)。在这种情况下,我不会花任何时间在 GC 调优上,因为那 10 GC/s 仍然存在。

3 - 一些经验

我在创建大量给定类的应用程序上遇到了类似的问题。在 GC 日志中,我注意到应用程序的创建速率约为 3 GB/s,这太高了(来吧……每秒 3 GB 的数据?!)。

问题:创建的对象太多导致频繁的GC。

在我的例子中,我附加了一个内存分析器,并注意到一个类代表了我所有对象的很大一部分。我追踪了实例,发现这个类基本上是一对包裹在对象中的布尔值。在这种情况下,有两种解决方案可用:

重新设计算法,这样我就不会返回一对布尔值,而是有两种方法分别返回每个布尔值

缓存对象,知道只有 4 个不同的实例

我选择了第二个,因为它对应用程序的影响最小并且易于引入。我花了几分钟的时间来放置一个带有非线程安全缓存的工厂(我不需要线程安全,因为我最终只有 4 个不同的实例)。

分配速率下降到 1 GB/s,年轻 GC 的频率也下降了(除以 3)。

希望有帮助!


3
3 revs, 2 users 69%

如果你只有值对象(也就是说,没有对其他对象的引用),但我的意思是真的有很多,你可以使用直接 ByteBuffers 和本机字节顺序[后者很重要],你需要一些一百行代码来分配/重用+ getter/setter。吸气剂看起来类似于 long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

只要您只分配一次,即一大块然后自己管理对象,那几乎可以完全解决 GC 问题。您只需索引(即 int)到必须传递的 ByteBuffer 中,而不是引用。您可能还需要自己调整内存。

该技术感觉就像使用 C and void*,但有一些包装是可以忍受的。如果编译器未能消除它,性能下降可能是边界检查。如果您像向量一样处理元组,一个主要的好处是局部性,缺少对象头也减少了内存占用。

除此之外,您可能不需要这样的方法,因为几乎所有 JVM 的年轻一代都会微不足道地死去,并且分配成本只是一个指针碰撞。如果您使用 final 字段,分配成本可能会更高一些,因为它们在某些平台(即 ARM/Power)上需要内存栅栏,但在 x86 上它是免费的。


N
Nitsan Wakart

假设您发现 GC 是一个问题(正如其他人指出的那样),您将为您的特殊情况实施自己的内存管理,即遭受大量流失的类。试一试对象池,我见过效果很好的案例。实现对象池是一条老路,因此无需重新访问这里,请注意:

多线程:使用线程本地池可能适用于您的情况

支持数据结构:考虑使用 ArrayDeque,因为它在删除时表现良好并且没有分配开销

限制池的大小:)

测量之前/之后等


P
Peter Mortensen

我遇到了类似的问题。首先,尽量减小小物体的尺寸。我们在每个对象实例中引入了一些引用它们的默认字段值。

例如,MouseEvent 有对 Point 类的引用。我们缓存点并引用它们,而不是创建新实例。例如,空字符串也是如此。

另一个来源是多个布尔值,它们被一个 int 替换,对于每个布尔值,我们只使用 int 的一个字节。


只是出于兴趣:它给你带来了什么表现?您是否在更改前后对您的应用程序进行了分析,如果是,结果如何?
@Axel 对象使用的内存要少得多,因此不会经常调用 GC。当然,我们分析了我们的应用程序,但速度提高甚至有视觉效果。
P
Peter Mortensen

前段时间我用一些 XML 处理代码处理过这种情况。我发现自己创建了数百万个非常小(通常只是一个字符串)且寿命极短的 XML 标记对象(XPath 检查失败意味着不匹配,因此丢弃)。

我做了一些认真的测试,得出的结论是,使用废弃标签列表而不是制作新标签,我只能将速度提高约 7%。但是,一旦实现,我发现空闲队列需要添加一个机制来修剪它,如果它变得太大 - 这完全取消了我的优化,所以我将它切换到一个选项。

总而言之-可能不值得-但我很高兴看到您正在考虑它,这表明您在乎。


D
David Plumpton

鉴于您正在编写一个国际象棋程序,您可以使用一些特殊的技术来获得良好的性能。一种简单的方法是创建一个大型长数组(或字节)并将其视为堆栈。每次移动生成器创建移动时,它都会将几个数字压入堆栈,例如从方格移动到方格。当您评估搜索树时,您将弹出移动并更新棋盘表示。

如果您想要表现力,请使用对象。如果您想要速度(在这种情况下),请使用本机。


r
rkj

我用于此类搜索算法的一种解决方案是仅创建一个 Move 对象,用新的移动对其进行变异,然后在离开范围之前撤消移动。您可能一次只分析一个动作,然后将最佳动作存储在某个地方。

如果由于某种原因这不可行,并且您想减少峰值内存使用量,那么这里有一篇关于内存效率的好文章:http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java-tutorial.pdf


死链接。那篇文章还有其他来源吗?
g
gyorgyabraham

只需创建数百万个对象并以正确的方式编写代码:不要保留对这些对象的不必要引用。 GC 会为你完成这项肮脏的工作。您可以使用前面提到的详细 GC 来查看它们是否真的被 GC 处理。 Java 是关于创建和释放对象的。 :)


抱歉,伙计,我不同意您的方法... Java 与任何编程语言一样,都是在其约束范围内解决问题,如果 OP 受 GC 约束,您将如何帮助他?
我在告诉他 Java 是如何工作的。如果他无法避免拥有数百万个临时对象的情况,最好的建议可能是临时类应该是轻量级的,并且他必须确保他尽快释放引用,而不是一步。我错过了什么吗?
Java 支持创建垃圾,并且会为您清理它,这是真的。如果 OP 无法避开对象的创建,并且他对在 GC 中花费的时间不满意,那将是一个悲伤的结局。我反对您提出的为 GC 做更多工作的建议,因为这在某种程度上是正确的 Java。
l
luke1985

我认为您应该阅读 Java 中的堆栈分配和逃逸分析。

因为如果你深入这个话题,你可能会发现你的对象甚至没有在堆上分配,它们也没有像堆上的对象那样被 GC 收集。

维基百科解释了逃逸分析,并举例说明了它在 Java 中的工作原理:

http://en.wikipedia.org/wiki/Escape_analysis


I
Ilya Gazman

我不是 GC 的忠实粉丝,所以我总是尝试寻找解决方法。在这种情况下,我建议使用 Object Pool pattern

这个想法是通过将它们存储在堆栈中来避免创建新对象,以便以后可以重用它。

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

对小对象使用池是一个非常糟糕的主意,每个线程都需要一个池来启动(或者共享访问会扼杀任何性能)。这样的池的性能也比一个好的垃圾收集器差。最后:处理并发代码/结构时,GC 是天赐之物——许多算法更容易实现,因为自然不存在 ABA 问题。参考。在并发环境中计数至少需要一个原子操作 + 内存栅栏(x86 上的 LOCK ADD 或 CAS)
管理池中的对象可能比让垃圾收集器运行更昂贵。
@ThorbjørnRavnAndersen 一般来说,我同意你的观点,但请注意,检测这种差异是一个相当大的挑战,当你得出结论认为 GC 在你的情况下效果更好时,如果这种差异很重要,它一定是一个非常独特的情况。无论如何,对象池可能会保存您的应用程序。
我根本不明白你的论点? GC是否比对象池快很难检测?因此你应该使用对象池? JVM 针对干净的编码和短暂的对象进行了优化。如果这些就是这个问题的目的(我希望如果 OP 每秒生成一百万个),那么只有在有可证明的优势来切换到您建议的更复杂且容易出错的方案时才应该如此。如果这太难证明,那又何必费心呢。
M
Michael Röschter

与堆上的对象分配相比,对象池提供了巨大的(有时是 10 倍)改进。但是上面使用链表的实现既幼稚又错误!链表创建对象来管理其内部结构,从而使工作无效。使用对象数组的 Ringbuffer 效果很好。在示例给出(管理移动的国际象棋程序)中,Ringbuffer 应该被包装到一个持有者对象中,以获取所有计算移动的列表。然后只有移动持有者对象引用会被传递。