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

Hum*_*mer 109 java garbage-collection

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

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

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

绝大多数对象都是非常短暂的,但生成的大约1%的移动是持久化并作为持久值返回,因此任何池化或缓存技术都必须能够排除特定对象的重用.

我不希望完整的示例代码,但我希望进一步阅读/研究的建议,或类似性质的开源示例.

Nie*_*sen 47

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

java -verbose:gc
Run Code Online (Sandbox Code Playgroud)

并且会在收集时告诉您.将有两种类型的扫描,快速扫描和全扫描.

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

箭头在尺寸之前和之后.

只要它只是做GC而不是一个完整的GC你就安全了.常规GC是"年轻一代"中的副本收集器,因此不再引用的对象只是被遗忘,这正是您想要的.

阅读Java SE 6 HotSpot虚拟机垃圾收集调整可能会有所帮助.


Mik*_*ail 21

从版本6开始,JVM的服务器模式采用了转义分析技术.使用它可以避免GC一起使用.

  • 如果您有使用这些选项的经验:-XX:+ PrintEscapeAnalysis和-XX:+ PrintEliminateAllocations.分享会很棒.因为我没有,说实话. (2认同)

Pie*_*rte 18

好吧,这里有几个问题!

1 - 如何管理短期对象?

如前所述,JVM可以完美地处理大量短期对象,因为它遵循弱代假设.

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

for(int i=0, i<max, i++) {
  // stuff that implies i
}
Run Code Online (Sandbox Code Playgroud)

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

因此,如果仅在本地使用它们,那么创建数百万个对象并不是什么大问题.它们在被存储在伊甸园之前就已经死了,所以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)也是如此.

希望有所帮助!


bes*_*sss 11

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

只要你只分配一次,即一个巨大的块然后自己管理对象,这几乎可以完全解决GC问题.而不是引用,你只有索引(即int)ByteBuffer,必须传递.您可能还需要自己对齐内存.

这种技术会像使用一样C and void*,但有一些包装它是可以忍受的.如果编译器无法消除它,性能下降可能会受到限制.如果你像向量一样处理元组,那么主要的好处就是局部性,缺少对象头也会减少内存占用.

除此之外,很可能你不需要这样的方法,因为几乎所有JVM的年轻一代都是琐碎的,而且分配成本只是一个指针.如果你使用final字段,因为它们在某些平台上需要内存栅栏(即ARM/Power),分配成本可能会高一些,但在x86上它是免费的.


Nit*_*art 8

假设您发现GC是一个问题(正如其他人指出它可能不是),您将为您的特殊情况实现自己的内存管理,即一个遭受大量流失的类.给对象池一个去,我已经看到它工作得很好的情况.实现对象池是一个很好的路径,所以不需要在这里重新访问,请注意:

  • 多线程:使用线程本地池可能适用于您的情况
  • 支持数据结构:考虑使用ArrayDeque,因为它在删除时表现良好,并且没有分配开销
  • 限制你的游泳池的大小:)

测量前/后等等


Sta*_*avL 6

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

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

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


Old*_*eon 6

我不久前用一些XML处理代码处理了这个场景.我发现自己创建了数百万个非常小的XML标记对象(通常只是一个字符串)而且非常短暂(XPath检查失败意味着不匹配,所以丢弃).

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

总结 - 可能不值得 - 但我很高兴看到你正在考虑它,它表明你关心.