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

时间:2013-05-07 12:36:32

标签: java garbage-collection

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

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

  1. 最大限度地减少垃圾收集的开销,
  2. 减少低端系统的峰值内存占用量。
  3. 绝大多数对象都是非常短暂的,但生成的大约1%的移动是持久化并作为持久值返回,因此任何池化或缓存技术都必须提供排除特定对象的能力重新使用。

    我不期望完全充实的示例代码,但我希望有进一步阅读/研究的建议,或类似性质的开源示例。

13 个答案:

答案 0 :(得分:47)

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

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 可能会有所帮助。

答案 1 :(得分:21)

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

答案 2 :(得分:18)

嗯,这里有几个问题!

1 - 如何管理短期对象?

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

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

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

让我们不要考虑循环展开(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)也是如此。

希望有所帮助!

答案 3 :(得分:11)

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

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

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

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

答案 4 :(得分:8)

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

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

等等之前/之后的测量

答案 5 :(得分:6)

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

例如,MouseEvent引用了Point类。我们缓存了Points并引用它们而不是创建新实例。例如,对于空字符串也一样。

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

答案 6 :(得分:6)

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

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

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

答案 7 :(得分:2)

鉴于您正在编写国际象棋程序,您可以使用一些特殊技术来获得不错的表现。一种简单的方法是创建一个大的long(或字节)数组并将其视为堆栈。每次你的移动生成器创建移动时,它会将几个数字推到堆栈上,例如从广场移动并移动到正方形。在评估搜索树时,您将弹出移动并更新电路板表示。

如果你想表达力量使用对象。如果你想要速度(在这种情况下)是原生的。

答案 8 :(得分:1)

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

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

答案 9 :(得分:0)

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

答案 10 :(得分:0)

我不是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
}

答案 11 :(得分:0)

我认为你应该阅读Java中的堆栈分配和转义分析。

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

有关于逃逸分析的维基百科解释,以及如何在Java中使用它的示例:

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

答案 12 :(得分:0)

对象池相对于堆上的对象分配提供了巨大的(有时10倍)改进。但是使用链表的上述实现既天真又错误!链表创建对象以管理其内部结构,从而使工作无效。 使用对象数组的Ringbuffer运行良好。在示例中,(国际象棋程序管理移动)应将Ringbuffer包装到所有计算移动列表的持有者对象中。然后只传递移动持有者对象引用。