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

什么是创造(和释放)数以百万计的小物体的“最佳实践”?

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

  1. 最大限度地减less垃圾收集的开销
  2. 降低低端系统的峰值内存占用。

绝大多数对象都是非常短暂的,但是大约1%的动作被持久化并作为持久值返回,所以任何池化或caching技术都必须提供排除特定对象被重用的能力。

我不希望完整的实例代码,但我会感谢进一步阅读/研究的build议,或开放源代码相似的例子。

运行具有详细垃圾回收的应用程序:

java -verbose:gc 

它会在收集时告诉你。 将有两种types的扫描,一个快速和一个完整的扫描。

 [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是“年轻一代”的collections者,所以不再被引用的对象只是被遗忘了,这正是你想要的。

阅读Java SE 6热点虚拟机垃圾收集调整可能是有帮助的。

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

那么这里有一个问题呢!

1 – 如何pipe理短期对象?

如前所述,JVM可以很好地处理大量的短期生命对象,因为它遵循弱生代假说 。

请注意,我们正在谈论到达主存(堆)的对象。 这并非总是如此。 你创build的很多对象甚至不会留下一个CPU寄存器。 例如,考虑这个for循环

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

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

因此,如果仅在本地使用,创build数百万个对象并不是什么大问题。 他们在被存放在伊甸园之前会死的,所以GC甚至不会注意到他们。

2 – 减lessGC的开销是否有用?

像往常一样,这取决于。

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

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

例如,如果你每100ms有一个年轻的GC,需要10ms,那么你花10%的时间在GC上,而你每秒钟有10个collection(这就是huuuuuge)。 在这种情况下,我不会花费任何时间进行GC调整,因为这10个GC / s仍然会在那里。

3 – 一些经验

我在创build大量给定类的应用程序上遇到类似的问题。 在GC日志中,我注意到应用程序的创build速度大约是3 GB / s,这太过分了(每秒3 GB的数据!)。

问题:由于创build的对象太多而频繁导致GC过多。

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

  • 重做algorithm,以便我不返回一对布尔值,而是有两个方法分别返回每个布尔值

  • caching对象,知道只有4个不同的实例

我select了第二个,因为它对应用程序的影响最小,很容易引入。 我花了几分钟的时间把工厂放在一个非线程安全的caching中(因为我最终只有4个不同的实例,所以我不需要线程安全)。

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

希望有所帮助!

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

只要你只分配一次,就可以解决GC问题,也就是说,一个庞大的块,然后自己pipe理对象。 而不是引用,你只需要索引(即int )到ByteBuffer中。 你可能需要做记忆以保持自己。

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

除此之外,很可能你并不需要像几乎所有JVM的年轻一代一样死亡,分配成本只是一个指针。 尽pipe如此,如果您使用final字段,因为它们在某些平台(即ARM / Power)上需要内存隔离,但是在x86上它可以是免费的,所以分配成本可能会稍高。

假设你发现GC是一个问题(正如其他人指出,它可能不是),你将实施你自己的内存pipe理你的特殊情况,即一个遭受大规模stream失的类。 给对象集合一个去,我已经看到它工作得很好的情况。 实现对象池是一个很好的走向path,所以不需要重新访问这里,注意:

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

之前/之后的测量等

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

例如,MouseEvent对Point类有一个引用。 我们caching点并引用它们而不是创build新的实例。 例如,对于空string也是如此。

另一个来源是多个布尔值,它们被一个intreplace,每个布尔值只使用int的一个字节。

我前段时间用一些XML处理代码处理了这个场景。 我发现自己创build了数以百万计的非常小的XML标签对象(通常只是一个string),而且非常短暂( XPath检查失败意味着不匹配如此丢弃)。

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

总之 – 可能不值得 – 但我很高兴看到你在考虑这件事,这表明你很在意。

鉴于您正在编写国际象棋程序,您可以使用一些特殊技巧来获得体面的performance。 一个简单的方法是创build一个长的(或字节)大数组,并将其视为一个堆栈。 每当你的移动发生器产生移动时,它将一些数字推到堆栈上,例如从正方形移动到正方形。 当您评估search树时,您将popup移动并更新棋盘表示。

如果你想expression能源使用对象。 如果你想速度(在这种情况下)本土化。

我用于这种searchalgorithm的一个解决scheme是创build一个Move对象,用新的移动对它进行变异,然后在离开范围之前撤消移动。 你可能一次只分析一个动作,然后在某个地方存储最好的动作。

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

只需创build您的数百万个对象,并以正确的方式编写代码:不要对这些对象进行不必要的引用。 GC会为你做肮脏的工作。 你可以像上面提到的那样使用详细的GC来查看它们是否真的GC'd。 Java是关于创build和释放对象的。 🙂

我想你应该阅读Java中的堆栈分配和逃逸分析。

因为如果你深入了解这个话题,你可能会发现你的对象甚至没有被分配到堆上,而且它们也不是以堆上的对象的方式被GC收集的。

有一个关于逃逸分析的维基百科解释,以及它如何在Java中工作的例子:

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

我不是GC的忠实粉丝,所以我总是试着find解决方法。 在这种情况下,我会build议使用对象池模式 :

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

 Class MyPool { LinkedList<Objects> stack; Object getObject(); // takes from stack, if it's empty creates new one Object returnObject(); // adds to stack } 

对象池对堆上的对象分配提供了巨大的(有时是10倍)改进。 但是上面的使用链表的实现既天真又错误! 链表创build对象来pipe理其内部结构,使努力无效。 使用对象数组的Ringbuffer运行良好。 在示例中,一个国际象棋程序pipe理动作,缓冲器应该被包裹到一个持有者对象中,以便计算所有移动的列表。 只有移动持有者对象引用才会被传递。