【问题标题】:How to really benchmark the memory usage of a Java application如何真正对 Java 应用程序的内存使用情况进行基准测试
【发布时间】:2015-03-06 01:47:56
【问题描述】:

我想比较 Java 程序的不同实现在内存使用效率方面。有不同的使用场景制定为 JUnit 测试用例。其实所有代码都是开源的:https://github.com/headissue/cache2k-benchmark

获取Java程序已用内存的一般常识是:Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(),当然也可以使用JMX接口来获取这些值。

但是,已用内存的确定值并不可靠。可能的原因:

  • 可能有未收集的垃圾
  • 如果 GC 未进行压缩,则存在碎片

到目前为止,我尝试切换到串行 GC,并在读取值之前使用 Runtime.getRuntime().gc() 强制进行垃圾收集。我已将实验代码放在:https://github.com/cruftex/java-memory-benchmark

如果我在读取值之前调用了三个gc,我会得到这个输出(mvn test | grep loopCount with jdk1.7.0_51):

testBaseline1: used=1084168, loopCount=0, total=124780544
testBaseline2: used=485632, loopCount=0, total=124780544
testBaseline3: used=483760, loopCount=0, total=124780544
testBaseline4: used=483800, loopCount=0, total=124780544
testBaseline: used=484160, loopCount=0, total=124780544
test100MBytes: used=105341496, loopCount=0, total=276828160
test127MBytes: used=133653088, loopCount=0, total=469901312
test27MBytes: used=28795528, loopCount=0, total=317755392
test10MBytes: used=10969776, loopCount=0, total=124784640

四个gc 调用(签入)我得到:

testBaseline1: used=483072, loopCount=0, total=124780544
testBaseline2: used=483728, loopCount=0, total=124780544
testBaseline3: used=483768, loopCount=0, total=124780544
testBaseline4: used=483808, loopCount=0, total=124780544
testBaseline: used=483848, loopCount=0, total=124780544
test100MBytes: used=105341504, loopCount=0, total=276828160
test127MBytes: used=133653096, loopCount=0, total=469901312
test27MBytes: used=28795536, loopCount=0, total=139239424
test10MBytes: used=10969784, loopCount=0, total=124784640

因此经验表明,通过四次 GC 调用,结果似乎是正确的。 从 GC 统计输出中,我可以看到第一次 GC 填充了永久空间,第四次 GC 调用减少了它:

2015-01-08T02:30:35.069+0100: [Full GC2015-01-08T02:30:35.069+0100: [Tenured: 0K->1058K(83968K)
2015-01-08T02:30:35.136+0100: [Full GC2015-01-08T02:30:35.136+0100: [Tenured: 1058K->1058K(83968K)
2015-01-08T02:30:35.198+0100: [Full GC2015-01-08T02:30:35.198+0100: [Tenured: 1058K->1058K(83968K)
2015-01-08T02:30:35.263+0100: [Full GC2015-01-08T02:30:35.264+0100: [Tenured: 1058K->471K(83968K)

最终代码,获取内存使用值是:

try {
  Runtime.getRuntime().gc();
  Thread.sleep(55);
  Runtime.getRuntime().gc();
  Thread.sleep(55);
  Runtime.getRuntime().gc();
  Thread.sleep(55);
  Runtime.getRuntime().gc();
  Thread.sleep(55);
} catch (Exception ignore) { }
long _usedMem;
long _total;
long _total2;
long _count = -1;
// loop to get a stable reading, since memory may be resized between the method calls
do {
  _count++;
  _total = Runtime.getRuntime().totalMemory();
  try {
    Thread.sleep(12);
  } catch (Exception ignore) { }
  long _free = Runtime.getRuntime().freeMemory();
  _total2 = Runtime.getRuntime().totalMemory();
  _usedMem = _total - _free;
} while (_total != _total2);
System.out.println(_testName + ": used=" + _usedMem + ", loopCount=" + _count + ", total=" + _total);

我很不确定这种方法是否一直都能产生可靠的结果。所以有些问题:

  • 是否有一些最佳实践可以从 Java 程序中获得可靠且可比较的基准值?
  • 任何想法如何针对该用例调整(或实际失谐)GC?
  • 是否有可靠的来源和可靠的行为来解释所需的四次 GC 调用? (顺便说一句:java 8 的执行方式相同)
  • 有没有办法说 JVM:“尽最大可能进行垃圾收集,我会等”?
  • 一般来说,对于问题陈述,最“面向未来”和最可靠的解决方案可能是什么?

更新:

虽然上面的一些问题是 GC 相关的,但实际问题不是。我喜欢找出单个时间点的应用程序的内存使用情况。一种可能的解决方案是对所有对象进行深度搜索并总结大小。

更新 2:

同时,我写了一篇关于该问题的大量博客文章,讨论了如何测量实际内存使用情况的不同方法:

https://cruftex.net/2017/03/28/The-6-Memory-Metrics-You-Should-Track-in-Your-Java-Benchmarks.html

【问题讨论】:

    标签: java performance memory garbage-collection benchmarking


    【解决方案1】:

    我也在这个问题上苦苦挣扎,并想知道是否有任何标准方法。

    我能做的最好的事情就是告诉 JVM 尽最大努力收集垃圾,方法是在运行之后和下一次之前调用以下方法:

    GcFinalization.awaitFullGc();
    

    此方法来自 Guava test-lib 包,可以作为 Maven 依赖项添加为:

     <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava-testlib</artifactId>
        <version>18.0</version>
    </dependency>
    

    实现如下所示:

    public static void awaitFullGc() {
       final CountDownLatch finalizerRan = new CountDownLatch(1);
       WeakReference<Object> ref = new WeakReference<Object>(
          new Object() {
             @Override protected void finalize() { finalizerRan.countDown(); }
          });
    
       await(finalizerRan);
       awaitClear(ref);
    
       // Hope to catch some stragglers queued up behind our finalizable object
       System.runFinalization();
     }
    

    这给了我每次运行的非常一致的结果,并使 CPU 用户时间(来自ThreadMXBean)非常接近纳米时间(来自System.currentTimeMills)。在这些测量中,我主要关心的是运行时间,但与中间没有此调用的版本相比,内存使用情况也是一致的。

    【讨论】:

    • 感谢阿里指出这一点!是的,未决的最终确定也可能是我没有考虑过的问题。
    【解决方案2】:

    首先,您应该查看 JMH,了解应该如何进行正确的 Java 基准测试。

    调用Runtime.getRuntime().gc() 绝对是一种不好的做法 - 无论是在现实生活中还是在对 GC 进行基准测试时。举个至少一个原因,通过强制 GC 循环,您直接惩罚了任何 GC 算法的性能。

    此外,您无法通过让它们执行 ~4 GC 周期来比较各种 GC 算法。您应该运行适当的 GC 基准测试 - 请参阅 JMH,并且您至少需要运行相当长的时间 - 根据堆大小,这可能是 10 分钟或几个小时......

    我认为您最好的开始是长时间运行类似 JMH 的基准测试(约 30 分钟),收集 GC 日志并处理 GC 日志以获取各种统计信息...至少要从某种合理的比较开始。

    【讨论】:

    • 谢谢。但抱歉,问题不在于如何对 GC 算法进行基准测试。
    • 您询问“如何对内存使用进行基准测试”和“内存使用效率”,而在 Java 中,内存使用与 GC 性能直接相关。
    • 可能有误会。在这种情况下,问题不是速度。基准测试是关于比较性能指标。这可能是运行时、吞吐量、已用内存等。要计算(静态)已用内存量,您还可以迭代所有堆对象并将大小相加。
    【解决方案3】:

    我想比较 Java 程序的不同实现 他们的内存使用效率。

    一种选择是运行程序:

    -Xloggc:gc.log_impl1 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
    

    然后,切换到实现 2 并重新运行

    -Xloggc:gc.log_impl2 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
    

    然后下载HPjmeter,将两个文件加载到控制台并使用比较 gc 功能。图表中可能存在一些偏差,但您会很好地了解程序内存配置文件的不同之处。

    我不会尝试综合调用 GC。

    【讨论】:

      猜你喜欢
      • 2015-09-07
      • 2012-05-27
      • 1970-01-01
      • 2010-10-19
      • 2011-08-11
      • 2017-05-17
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多