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


109

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

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

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

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

我不希望完全编写出示例代码,但希望您提供进一步阅读/研究的建议,或者具有类似性质的开源示例。


11
轻量级模式是否适合您的情况?en.wikipedia.org/wiki/Flyweight_pattern
罗杰·罗兰

4
您是否需要将其封装在一个对象中?
nhahtdh

1
Flyweight模式不合适,因为对象不共享重要的公共数据。至于将数据封装在一个对象中,它太大了,无法打包到原语中,这就是为什么我正在寻找POJO的替代方法。
谦虚的程序员,

Answers:


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虚拟机垃圾收集调优可能会有所帮助。


试用Java堆大小,以尝试找到很少发生完全垃圾收集的点。在Java 7中,新的G1 GC在某些情况下更快(而在其他情况下则更慢)。
Michael Shopsin

21

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


1
转义分析通常令人失望,值得检查JVM是否确定您在做什么。
Nitsan Wakart

2
如果您有使用这些选项的经验,请:-XX:+ PrintEscapeAnalysis和-XX:+ PrintEliminateAllocations。分享那太好了。因为我没有,说实话。
米哈伊尔

参见stackoverflow.com/questions/9032519/…您将需要获取JDK 7的调试版本,我承认我没有这样做,但是对于JDK 6来说它已经成功了。
Nitsan Wakart

19

好吧,这里有几个问题!

1-如何管理短期对象?

如前所述,由于JVM遵循弱代假说,因此可以完美地处理大量短期对象。

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

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个集合(这是很酷的)。在这种情况下,我不会花任何时间进行GC调整,因为那10 GC / s仍会存在。

3-一些经验

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

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

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

  • 重做算法,这样我就不会返回一对布尔值,而是有两个分别返回每个布尔值的方法

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

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

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

希望能有所帮助!


11

如果您仅具有值对象(即,没有对其他对象的引用),但实际上我的意思是无数个对象,则可以将其ByteBuffers与本机字节顺序一起使用直接使用[后者很重要],并且需要几百行分配/重用+ getter / setter的代码。吸气剂看起来类似于long getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

只要您仅分配一次(即,分配一个很大的块)然后自己管理对象,那几乎可以解决GC问题。除了索引,您只需要在索引中(即intByteBuffer要传递的。您可能还需要使内存对齐。

该技术就像使用 C and void*,但是经过一些包装可以承受。如果编译器无法消除性能下降的可能,那就是性能下降。如果您像处理向量那样的元组,则主要的缺点是局部性,缺少对象标头也减少了内存占用。

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


8

假设您发现GC是一个问题(正如其他人指出的那样),您将为特殊情况(即遭受大量混乱的类)实现自己的内存管理。尝试一下对象池,我已经看到了很好的例子。实现对象池是一条很容易破解的路径,因此无需在这里重新访问,请注意:

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

测量之前/之后等


6

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

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

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


只是出于兴趣:它为您带来了什么明智的业绩?您是否在更改前后对应用程序进行了概要分析,如果是,结果是什么?
Axel

@Axel对象使用的内存要少得多,因此不会经常调用GC。绝对可以,我们对应用程序进行了配置,但速度的提高甚至带来了视觉效果。
StanislavL

6

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

我进行了一些认真的测试,得出的结论是,使用废弃标签列表而不是制作新标签只能使速度提高大约7%。但是,一旦实施,我发现空闲队列需要添加一种机制来修剪它(如果它太大)-这完全使我的优化无效,因此我将其切换为选项。

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


2

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

如果要表达力量,请使用物体。如果您想提高速度(在这种情况下),请选择本机。


1

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

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


无效链接。该文章还有其他资料来源吗?
dnault

0

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


1
抱歉,我不同意您的方法... Java,就像任何编程语言一样,是关于在其约束内解决问题,如果OP受GC约束,您将如何帮助他?
Nitsan Wakart

1
我告诉他Java实际上是如何工作的。如果他不能回避拥有数百万个临时对象的情况,最好的建议可能是,临时类应该是轻量级的,并且他必须确保他尽快释放引用,而不是一步之遥。我想念什么吗?
gyorgyabraham 2013年

Java支持创建垃圾,并会为您清理垃圾,这是真的。如果OP无法躲避对象的创建,并且他对在GC上花费的时间不满意,那将是一个可悲的结局。我的反对意见是您提出的建议,为GC做更多工作,因为这在某种程度上是正确的Java。
Nitsan Wakart

0

我认为您应该阅读有关Java中的堆栈分配和转义分析的信息。

因为如果您更深入地研究该主题,您可能会发现对象甚至没有在堆上分配,并且GC无法像堆上的对象那样收集对象。

维基百科对转义分析进行了说明,并举例说明了其在Java中的工作方式:

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


0

我不是GC的忠实拥护者,因此我总是尝试寻找解决方法。在这种情况下,我建议使用对象池模式

这样做是为了避免通过将新对象存储在堆栈中来创建新对象,以便以后可以重用它。

Class MyPool
{
   LinkedList<Objects> stack;

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

3
对小型对象使用池是一个很糟糕的主意,您需要为每个线程分配一个池来启动(否则共享访问会降低性能)。这样的池还比一个好的垃圾收集器性能差。最后:处理并发代码/结构时,GC真是天赐之物-许多算法很容易实现,因为自然不会出现ABA问题。参考 在并发环境中进行计数至少需要一个原子操作+内存屏障(x86上的LOCK ADD或CAS)
bestss13年

1
池中对象的管理可能比让垃圾收集器运行昂贵。
托尔比约恩Ravn的安徒生

@ThorbjørnRavnAndersen通常我同意您的意见,但是请注意,发现这种差异是一个挑战,当您得出结论说GC在您的情况下效果更好时,如果这种差异很重要,那肯定是一个非常独特的情况。反之,对象池可能会保存您的应用程序。
Ilya Gazman '16

1
我根本不理解您的观点?很难检测GC是否比对象池更快?因此,您应该使用对象池吗?JVM已针对干净编码和短寿命对象进行了优化。如果这些问题是关于此问题的(我希望如果OP每秒产生一百万个问题),那么只有在您有可证明的优势转用您建议的更复杂且易于出错的方案时,才应该这样做。如果这很难证明,那为什么要麻烦。
托尔比约恩Ravn的安徒生

0

对象池相对于堆上的对象分配提供了极大的改进(有时是10倍)。但是上述使用链表的实现既幼稚又错误!链表创建对象来管理其内部结构,从而使工作无效。使用对象数组的Ringbuffer可以很好地工作。在给定(由棋子程序管理动作)的示例中,应将Ringbuffer包装到所有计算出的动作列表的Holder对象中。然后,仅移动持有者对象引用将被传递。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.