111

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

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

  1. 最小化垃圾收集的开销,以及
  2. 减少低端系统的峰值内存占用。

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

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

4

13 回答 13

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

于 2013-05-07T13:04:19.620 回答
21

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

于 2013-05-07T13:25:23.767 回答
19

好吧,这里有几个问题!

1 - 如何管理短期对象?

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

请注意,我们说的是到达主内存(堆)的对象。这并非总是如此。您创建的许多对象甚至没有留下 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)。

希望有帮助!

于 2013-05-12T21:54:14.540 回答
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 上它是免费的。

于 2013-05-08T07:56:01.047 回答
8

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

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

测量之前/之后等

于 2013-05-07T21:24:49.067 回答
6

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

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

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

于 2013-05-07T22:42:23.457 回答
6

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

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

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

于 2013-05-07T13:59:59.073 回答
2

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

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

于 2013-05-13T08:28:39.387 回答
1

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

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

于 2013-05-11T00:06:35.660 回答
0

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

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

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}
于 2013-05-12T14:29:49.360 回答
0

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

于 2013-05-09T09:45:40.303 回答
0

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

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

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

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

于 2014-01-31T20:22:12.550 回答
0

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

于 2017-03-18T11:13:31.683 回答