2013-05-07 62 views
105

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

我正在用Java编写国际象棋程序,搜索算法为每个可能的移动生成一个“移动”对象,标称搜索可以轻松生成每秒超过一百万个移动对象。在JVM GC已经能够处理在我的开发系统的负荷,但我很感兴趣,探索不同的途径将:

  1. 尽量减少垃圾收集的开销,
  2. 降低峰值内存占用低端系统。

绝大多数的对象是非常短暂的,但所产生的移动的约1%被持久和返回作为持久值,所以任何池或高速缓存技术将必须提供排除特定的能力对象被重用。

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

+11

Flyweight模式适合您的情况吗? http://en.wikipedia.org/wiki/Flyweight_pattern – 2013-05-07 12:38:41

+4

你需要将它封装在一个对象中吗? – nhahtdh 2013-05-07 12:38:51

+1

Flyweight Pattern不适用,因为对象不共享重要的公共数据。至于将数据封装在一个对象中,它太大而无法包装到一个原语中,这就是为什么我正在寻找POJO的替代品。 – 2013-05-07 12:43:51

回答

46

运行带有详细垃圾收集应用程序:

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可能是有帮助的。

+0

尝试使用Java堆大小尝试查找完整垃圾回收的点罕见。在Java 7中,新的[G1](http://www.drdobbs.com/jvm/g1-javas-garbage-first-garbage-collector/219401061?pgno = 2)GC在某些情况下速度更快(而其他情况下速度更慢)。 – 2013-05-08 15:21:14

21

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

+1

转义分析经常令人失望,值得检查一下JVM是否已经知道你在做什么或不在。 – 2013-05-08 08:09:42

+2

如果您有使用这些选项的经验:-XX:+ PrintEscapeAnalysis和-XX:+ PrintEliminateAllocations。分享这很棒。因为我没有,诚实地说。 – Mikhail 2013-05-08 08:13:30

+0

请参阅http://stackoverflow.com/questions/9032519/eligibility-for-escape-analysis-stack-allocation-with-java-7您需要为JDK 7获得调试版本,我承认我还没有完成但是使用JDK 6,它已经成功了。 – 2013-05-08 08:30:14

6

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

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

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

+0

只是出于兴趣:它是什么让你表现明智?在变更之前和之后是否对应用程序进行了描述,如果是,结果如何? – Axel 2013-05-07 14:12:51

+0

在这种情况下,实习字符串可能是一个好主意。 – 2013-05-07 18:18:42

+0

@Axel对象使用更少的内存,所以GC不经常被调用。当然,我们对我们的应用程序进行了描述,但速度提升的效果甚至还有。 – StanislavL 2013-05-08 05:37:46

8

假设你发现GC是一个问题(其他人指出它可能不是),你将为你的特殊情况实施你自己的内存管理,即遭受大量客户流失的类。给对象池一个去,我见过它很好地工作的情况。实施对象池是一个很好走过的道路所以没有必要在这里重新登陆,寻找出:

  • 多线程:使用线程本地池可能适用于你的情况
  • 后台数据结构:考虑使用ArrayDeque因为它删除即可顺利执行且没有分配开销
  • 限制你的池的大小:)

测量前/后等,等

6

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

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

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

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上它是免费的。

0

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

+1

对不起伙伴,我不同意你的方法......和其他编程语言一样,Java就是要解决一个约束内的问题,如果OP受到GC的约束,你如何帮助他? – 2013-05-09 17:26:12

+1

我告诉他Java是如何工作的。如果他无法躲避拥有数百万个临时对象的情况,那么最好的建议是,临时类应该是轻量级的,他必须确保尽快释放引用,而不是一个单一的步骤。我错过了什么吗? – gyorgyabraham 2013-05-22 10:51:08

+0

Java支持创建垃圾,并且会为您清理垃圾,这是事实。如果OP不能躲避物体的创造,而且他对在GC上花费的时间不满意,那么这是一个悲伤的结局。我的反对意见是你为GC做出更多工作所提出的建议,因为这是某种合适的Java。 – 2013-05-22 19:09:21

1

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

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

+0

死链接。那篇文章有其他来源吗? – dnault 2017-08-02 22:04:23

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 
} 
+3

对于小对象使用池是个不错的主意,每个线程需要一个池来引导(或者共享访问会杀死任何性能)。这样的池也比一个好的垃圾收集器表现得更差。最后:在处理w /并发代码/结构时,GC是天赐之物 - 很多算法都很容易实现,因为没有ABA问题。参考。在并发环境中计数至少需要一个原子操作+内存栏(x86上的LOCK ADD或CAS) – bestsss 2013-05-12 19:56:12

+1

池中对象的管理可能比让垃圾收集器运行更昂贵。 – 2016-04-18 17:35:50

+0

@ThorbjørnRavnAndersen一般来说,我同意你的看法,但要注意检测这种差异是一个相当大的挑战,而当你确定GC在你的案例中效果更好时,如果这种差异很重要,那么它必定是一个非常独特的案例。反过来,它可能是对象池将保存你的应用程序。 – 2016-04-18 17:49:16

18

好吧,这里有一个问题在这里!

1 - 如何管理短期对象?

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

请注意,我们正在谈论达到主内存(堆)的对象。这并非总是如此。你创建的很多对象甚至不会留下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,否则,它可能并不值得。例如,如果你每100ms有一个需要10ms的年轻GC,那么你花10%的时间在GC上,并且每秒有10个集合(这就是huuuuuge)。在这种情况下,我不会花时间进行GC调优,因为这10个GC/s仍然会在那里。

3 - 一些经验

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

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

在我的情况下,我附加了一个内存分析器,并注意到一个类占我所有对象的很大一部分。我追踪了实例,发现这个类基本上是一对包裹在一个对象中的布尔值。在这种情况下,两种溶液可用:

  • 返修的算法,这样我不回一对布尔值,而是我有两个方法,返回各自的布尔分别

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

我选择了第二个,因为它对应用程序的影响最小,很容易引入。花了我几分钟的时间才将工厂的线程安全缓存(我不需要线程安全,因为我最终只有4个不同的实例)。

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

希望有帮助!

2

鉴于您正在编写一个国际象棋程序,您可以使用一些特殊的技巧来获得体面的表现。一种简单的方法是创建一个长阵列(或字节)并将其视为堆栈。每次移动生成器创建移动时,都会将一些数字压入堆栈,例如,从广场移动并移动到广场。在评估搜索树时,您将弹出移动并更新董事会代表。

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

0

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

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

有逃逸分析的维基百科的解释,以实例是如何工作的在Java中:

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

0

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