3

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

我在具有 4 个内核和 16GB RAM 的 Linux 上使用 Hotspot Java 1.7.0、64 位。以下 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]);
    }

更新: 在下面的讨论中,我意识到人们试图建议对代码进行一些更改。所以我需要多解释一下问题的背景。源示例是合成的虚幻代码。只是它很好地说明了很多老一代对象的问题。我在尝试实现具有一些插入和逐出策略的高负载缓存解决方案时遇到了这个问题。这往往会导致老一代垃圾的问题。我的目标是找到使用 JVM 选项的最佳解决方案。在这里我不想考虑代码改进。我想如果有一种神奇的 GC 参数组合使我的示例可以处理低于 100 毫秒的暂停,它也可能解决更通用的问题,或者至少为类似情况提供一些提示。

4

7 回答 7

2

我会尝试用 替换可怕的链表ArrayDequeue,特别是因为队列大小是恒定的。

对于以递归方式实现标记的垃圾收集器来说,一个极长的链表很容易导致性能问题。收集器可能对可以迭代标记的大数组更满意。

更新

有一个模糊的 GC 调整参数可能会有所帮助:

如果您将此参数设置得更大,如果标记是深度递归的(因为它很可能是一个巨大的链表) ,则可能足以阻止 CMS 收集器切换到非增量模式。

但是,这样做会增加 JVM 的整体内存使用量。我的“信封背面”的想法是,您需要一个至少 192 MB 的标记堆栈来标记具有 16M 个元素的链表。这需要乘以进行标记的 GC 线程数。


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

恐怕在这种情况下,您的目标可能无法实现(无法承受上述情况)。如果应用程序对 GC 不够友好,那么您将获得较差的 GC 性能。

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

于 2013-07-15T13:36:03.997 回答
2

我正在开发一个名为Banana的原始集合库,它支持原始链接列表。您的用例它几乎是 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内存如果不先运行banana函数自己运行会更好

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);
  }
}
于 2013-07-16T07:26:51.137 回答
1

您的示例几乎是大多数垃圾收集器的病态案例。解决该问题的更好方法是使用 Disruptor,但我从您的评论中看到您不想要替代设计建议。

如果您提供了 GC 日志,则可能有一些 CMS 调整选项可以使事情变得更好,但如果没有日志就很难判断。暂停是由于 FullGC 还是由于 Remark 阶段?如果是 FullGC,则可能是 CMS 启动得不够早,无法跟上。

您正在解决的真正问题是什么,因为人为的问题似乎有点疯狂?

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

于 2013-07-18T13:39:29.560 回答
1

在类似的场景中,我们通过 -Xmx 选项添加了两倍于所需数量的内存,并添加了 -XX:CMSInitiatingOccupancyFraction=50 -XX:+UseCMSInitiatingOccupancyOnly 选项。

在这种情况下,JVM 会在 old gen 已满 50% 时执行 GC(即我们强制提前 GC)并且它有足够的空闲内存来进行快速碎片整理。

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

于 2013-07-22T07:18:40.647 回答
0

Full GC 持续时间随对象引用的数量而变化。

你可能需要做两件事:

1) 可能的设计变更

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

  • 在对象进入队列时对其进行序列化。如果您无论如何都想将它们发送到外面,这将特别有趣。这会将 Full GC 减少到基本上为零。以https://code.google.com/p/fast-serialization/ OffHeapQueue (未发布)为例。

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

于 2013-07-19T14:37:18.090 回答
0

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

如果您真的只想调整 JVM 以按原样运行示例,我建议您切换到ParNew 收集器(使用)并通过设置和两者都设置为类似or来-XX:+UseParNewGC使新一代相当小。-XX:NewSize-XX:MaxNewSize200m300m

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

我刚刚发表了一篇博客文章,展示了(图 8 左侧)一个看起来与您的非常相似的微基准。在这种情况下,ParNewGC 大大优于其他收集器。

问候, 安德烈亚斯

于 2013-12-23T22:38:48.157 回答
0

您将在使用链表时遇到很多麻烦。

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

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

如果我在我的机器上按原样运行你的程序,我会得到 Full GC,因为它填充堆的速度比 CMS 清理它的速度要快。您不能真正根据这个小片段开始 JVM 调优练习。

希望有帮助!

于 2013-07-15T21:17:23.450 回答