【问题标题】:Best practice for creating millions of small temporary objects创建数百万个小型临时对象的最佳实践
【发布时间】:2013-05-07 12:36:31
【问题描述】:

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

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

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

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

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

【问题讨论】:

标签: java garbage-collection


【解决方案1】:

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

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

【讨论】:

  • 用 Java 堆大小进行实验,试图找到一个完全垃圾收集很少见的点。在 Java 7 中,新的G1 GC 在某些情况下更快(而在其他情况下更慢)。
【解决方案2】:

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

【讨论】:

  • 逃逸分析常常让人失望,值得检查一下JVM是否已经弄清楚你在做什么。
  • 如果您有使用这些选项的经验:-XX:+PrintEscapeAnalysis 和 -XX:+PrintEliminateAllocations。那将是伟大的分享。因为我没有,老实说。
  • 请参阅stackoverflow.com/questions/9032519/…,您需要为 JDK 7 获取调试版本,我承认我还没有这样做,但使用 JDK 6 已经成功了。
【解决方案3】:

好吧,这里有几个问题!

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 寄存器中,并定期对其进行递增但永远不会将其发送回主存。

因此,如果仅在本地使用,创建数百万个对象并不是什么大问题。它们在存储到 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,young GC 的频率也下降了(除以 3)。

希望有帮助!

【讨论】:

    【解决方案4】:

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

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

    该技术感觉就像使用C and void*,但有一些包装是可以忍受的。如果编译器无法消除它,性能下降可能是边界检查。如果您像向量一样处理元组,一个主要的好处是局部性,缺少对象头也减少了内存占用。

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

    【讨论】:

      【解决方案5】:

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

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

      测量之前/之后等等等等

      【讨论】:

        【解决方案6】:

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

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

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

        【讨论】:

        • 只是出于兴趣:它对您的性能有何好处?您是否在更改前后分析过您的应用程序?如果是,结果如何?
        • @Axel 对象使用的内存要少得多,因此不会经常调用 GC。毫无疑问,我们分析了我们的应用程序,但速度提升甚至产生了视觉效果。
        【解决方案7】:

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

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

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

        【讨论】:

          【解决方案8】:

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

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

          【讨论】:

            【解决方案9】:

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

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

            【讨论】:

            • 死链接。那篇文章还有其他来源吗?
            【解决方案10】:

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

            【讨论】:

            • 对不起,伙计,我不同意你的方法...... Java 和任何编程语言一样,都是在其约束范围内解决问题,如果 OP 受 GC 约束,你如何帮助他?跨度>
            • 我在告诉他 Java 是如何工作的。如果他无法避免拥有数百万个临时对象的情况,最好的建议可能是,临时类应该是轻量级的,他必须确保他尽快释放引用,而不是一步一步。我错过了什么吗?
            • Java 支持创建垃圾,并且会为您清理它,这是真的。如果 OP 无法避开对象的创建,并且他对在 GC 中花费的时间不满意,那将是一个悲伤的结局。我反对你为 GC 做更多工作的建议,因为这在某种程度上是正确的 Java。
            【解决方案11】:

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

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

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

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

            【讨论】:

              【解决方案12】:

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

              【讨论】:

              • 对小对象使用池是一个非常糟糕的主意,您需要为每个线程启动一个池(否则共享访问会扼杀任何性能)。这样的池的性能也比一个好的垃圾收集器差。最后:处理并发代码/结构时,GC 是天赐之物——许多算法更容易实现,因为自然不存在 ABA 问题。参考。在并发环境中计数至少需要一个原子操作 + 内存栅栏(x86 上的 LOCK ADD 或 CAS)
              • 管理池中的对象可能比让垃圾收集器运行的成本更多
              • @ThorbjørnRavnAndersen 一般来说,我同意你的观点,但请注意,检测这种差异是一个相当大的挑战,当你得出结论认为 GC 在你的情况下效果更好时,它必须是一个非常独特的情况,如果这种差异很重要。无论如何,对象池可能会保存您的应用程序。
              • 我根本不明白你的论点? GC是否比对象池快很难检测?因此你应该使用对象池? JVM 针对干净的编码和短暂的对象进行了优化。如果这些就是这个问题的目的(我希望如果 OP 每秒生成一百万个),那么只有在有可证明的优势来切换到您建议的更复杂且容易出错的方案时才应该如此。如果这太难证明,那何必呢。
              【解决方案13】:

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2019-11-23
                • 1970-01-01
                • 2011-12-07
                • 1970-01-01
                • 1970-01-01
                • 2018-07-07
                • 1970-01-01
                相关资源
                最近更新 更多