GC暂停调整长FIFO队列应用程序

时间:2013-07-15 13:04:17

标签: java performance garbage-collection

我有一个应用程序,它将一些数据放入一个占用JVM中几乎所有JVM内存的长链表中。插入新元素时,将删除最后一个元素,以使列表的大小始终为常量。当我将JVM内存大小设置为6GB时,我开始定期进行GC暂停:3.4秒,大约每10秒发生一次。

我在Linux上使用Hotspot Java 1.7.0,64位,4核和16GB RAM。传递以下JVM参数:   -Xmx6g -XX:+ PrintGC -XX:+ UseConcMarkSweepGC -XX:+ UseParNewGC

请问一些更好的选择,以便将GC暂停时间最小化到大约100毫秒左右?我试图找到这样的选择,但没有成功。

资料来源如下:

    LinkedList<long[]> list = new LinkedList<long[]>();

    // initial fill in
    for(int i = 0; i < 16L*1024*1024; i ++) {
        list.add(new long[16]);
    }

    System.out.printf("total: %5.1f free: %5.1f\n",((float)Runtime.getRuntime().totalMemory())/(1024*1024*1024), ((float)Runtime.getRuntime().freeMemory())/(1024*1024*1024));

    // the main stuff
    for(;;) {
        list.removeFirst();
        list.add(new long[16]);
    }

更新 在下面的讨论中,我意识到人们正试图在代码中提出一些变化。所以我需要在问题的背景上解释一下。源示例是合成的虚幻代码。它恰好说明了许多旧的gen对象的问题。在尝试实现具有一些插入和逐出策略的高负载缓存解决方案时,我遇到了这个问题。这经常导致旧的垃圾问题。我的目标是使用JVM选项找到最佳解决方案。在这里,我不想考虑代码改进。我想如果有一种神奇的GC参数组合使我的例子能够处理100ms以下的暂停,那么它也可以解决更普遍的问题,或者至少为类似情况提供一些提示。

7 个答案:

答案 0 :(得分:2)

我尝试用ArrayDequeue替换怪异的链表,特别是因为队列大小是不变的。

极长的链表可能会导致垃圾收集器出现性能问题,垃圾收集器会递归执行标记。对于可以迭代标记的大型数组,收集器可能会更开心。

<强>更新

有一个不起眼的GC调整参数可能会有所帮助:

如果你把这个参数做得更大,那么可能就足以阻止CMS收集器在非常递归的情况下进入非增量模式(因为它可能是一个巨大的链接-list。)

然而,这样做会增加JVM的整体内存使用量。我的“信封背后”的想法是你需要一个至少192 MB的标记堆栈来标记一个包含16M元素的链表。这需要乘以标记的GC线程数。


  

我的问题的目标不是改变Java代码。想象一下,你有一个正确的java程序,它不会导致OutOfMemoryError。您必须在不更改代码的情况下找到正确的JVM参数。实际上我已经理解了这种暂停的原因,只是我不知道如何调整JVM以使暂停时间小于100毫秒。

我担心在那种情况下你的目标可能是无法实现的(不能承受上述情况)。如果某个应用程序对GC不友好,那么GC性能会很差。

无论如何,你的更大目标是(应该)以任何必要的方式解决性能问题。修复程序。在这种情况下,修复可能还有其他好处;例如减少内存使用量。

答案 1 :(得分:2)

我正在开发一个名为Banana的原始集合库,它支持原始linked lists。 你的用例它几乎是Banana闪耀的理想用例,但它可以做得更多(包括你可以使用的可变长度块,但可能在你的真实世界中使用)。

这是我的计算机上此基准测试的结果:

Banana : 1269 ms elapsed
Banana : total:   2.5 GB,  free:   0.5 GB, used =   2.1 GB, Banana reports that it's actually using   2.1 GB
Java : 13543 ms elapsed
Java : total:   6.2 GB,  free:   2.0 GB, used =   4.2 GB

你可以看到Banana更快,并且使用更少的内存。 (如果你自己运行它而不先运行香蕉函数

,Java内存会更好
Java : 14426 ms elapsed
Java : total:   5.8 GB,  free:   1.9 GB, used =   3.9 GB

但香蕉附近仍然没有。

package net.yadan.banana.list;

public class LinkedListBenchmark {
  public static void main(String[] args) {
    banana();
    java();
  }

  public static void banana() {
    long t = System.currentTimeMillis();

    // initial list size 16m records, block size 32 (storage is int[], so we
    // need 32 ints to hold 16 longs)
    net.yadan.banana.list.LinkedList list = new LinkedList(16 * 1024 * 1024, 16 * 2, 0);

    // initial fill in
    for (int i = 0; i < 16L * 1024 * 1024; i++) {
      list.appendTail(32); // similar to java list.add() which appends to the
                           // end of the list
    }

    // the main stuff
    for (int i = 0; i < 16L * 1024 * 1024; i++) {
      list.removeHead(); // similar to java list removeFirst()
      list.appendTail(32); // similar to java list.add() which appends to the
                           // end of the list
    }

    System.out.println("Banana : " + (System.currentTimeMillis() - t) + " ms elapsed");
    float GB = 1024 * 1024 * 1024;
    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out
        .printf(
            "Banana : total: %5.1f GB,  free: %5.1f GB, used = %5.1f GB, Banana reports that it's actually using %5.1f GB\n",
            total / GB, free / GB, (total - free) / GB, list.computeMemoryUsage() / GB);
  }

  public static void java() {

    long t = System.currentTimeMillis();

    java.util.LinkedList<long[]> list = new java.util.LinkedList<long[]>();

    // initial fill in
    for (int i = 0; i < 16L * 1024 * 1024; i++) {
      list.add(new long[16]);
    }

    // the main stuff
    for (int i = 0; i < 16L * 1024 * 1024; i++) {
      list.removeFirst();
      list.add(new long[16]);
    }

    System.out.println("Java : " + (System.currentTimeMillis() - t) + " ms elapsed");
    float GB = 1024 * 1024 * 1024;
    long total = Runtime.getRuntime().totalMemory();
    long free = Runtime.getRuntime().freeMemory();
    System.out.printf("Java : total: %5.1f GB,  free: %5.1f GB, used = %5.1f GB\n", total / GB, free / GB,
        (total - free) / GB);
  }
}

答案 2 :(得分:1)

你的例子几乎是大多数垃圾收集者的病态案例。解决这个问题的一个更好的解决方案是使用Disruptor,但我从你的评论中看到你不想要另一种设计建议。

如果您提供了GC日志,可能会有一些CMS调整选项可以使事情变得更好但是没有日志就很难分辨。暂停是由于FullGC还是由于备注阶段?如果是FullGC,可能是CMS没有及早启动以便跟上。

你正在解决的真正问题是什么,因为人为的问题看起来有点疯狂?

如果你想拥有这样的设计模式,那么最好的JVM就是Azul Zing。

答案 3 :(得分:1)

在类似的情况下,我们通过-Xmx选项添加了尽可能多的内存,作为所需金额的两倍 并补充说 -XX:CMSInitiatingOccupancyFraction = 50 -XX:+ UseCMSInitiatingOccupancyOnly 选项。

在这种情况下,JVM在旧gen为50%时执行GC(即我们强制使用更早的GC)并且它有足够的可用内存来进行快速碎片整理。

此外,您可能会发现这些标志 -XX:+ CMSConcurrentMTEnabled -XX:+ CMSScavengeBeforeRemark 也很有用。

答案 4 :(得分:0)

链接列表会给你带来很多麻烦。

当GC开始扫描堆以查找活动对象时,它从列表的第一个元素开始,然后以递归方式遍历每个元素。如果您的列表包含许多元素,则意味着无论如何,GC暂停都会很长。

您应该将list声明为Collection<long[]>然后尝试不同的实现,例如基于ArrayDequeue的手动制作的圆形数组的ArrayList

如果我在我的机器上按原样运行你的程序,我将获得Full GC,因为它填充堆的速度比CMS能够清理它更快。你无法真正开始基于这个小片段的JVM调优练习。

希望有所帮助!

答案 5 :(得分:0)

完整GC持续时间与对象引用量一起缩放。

你可能需要做两件事:

1)可能的设计变更

  • 使用基于数组的集合,如ArrayDeque(少数特效)

  • 在对象进入队列时序列化。这特别有趣 如果你想把它们送到外面。这将基本上减少Full GC 零。结帐https://code.google.com/p/fast-serialization/ OffHeapQueue(未发布)的例子。

2) 我已经使用合成应用程序对如何调整GC以获取大型静态数据进行了一些分析。 http://java-is-the-new-c.blogspot.com/这可能会有所帮助或节省一些时间

答案 6 :(得分:0)

正如其他评论者已经指出你的样本打破了世代垃圾收集的基本先决条件,即“大多数物品年轻”。您将所有对象保持活动的时间相似,这会使分代GC效率低下。

如果您真的只想调整JVM以运行示例,我建议您切换到 ParNew收集器(使用-XX:+UseParNewGC)并使新一代相当小将-XX:NewSize-XX:MaxNewSize设置为200m300m

请注意,没有像 CMS G1 这样的并发收集器可以帮助您,因为始终存在并发模式故障并且回退到Full GC。 (Java 7中的默认设置) ParallelGC收集器也会提供较差的性能,因为它隐式使用-XX:+UseParallelOldGC并且单个LinkedList的遍历(用于标记)不适合并行化。

我刚刚发布a blog article显示(在图8中,左侧)微观基准,看起来与您的非常相似。在这种情况下, ParNewGC 大幅超越其他收藏家。

此致 安德烈亚斯