如何真正基准测试Java应用程序的内存使用情况

9
我想要比较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调用,则会得到以下输出(对于jdk1.7.0_51,使用mvn test | grep loopCount):
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

3个回答

5

我也曾遇到这个问题,并且很想知道是否有任何标准方法。

我所能做的最好的办法是,在运行后并在下一次运行之前调用以下方法,告诉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)。对于这些测量,我的主要关注点是运行时间,但与没有此调用版本相比,内存使用也很稳定。


谢谢Ali指出这个问题!是的,未最终确定可能也是我没有考虑到的一个问题。 - cruftex

0
我想比较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。


-2

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

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

此外,您不能仅仅执行约4个GC周期来比较各种GC算法。您应该运行适当的GC基准测试-请参见JMH,并且您需要运行至少相当长的时间-这取决于堆大小,可能是10分钟或几个小时...

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


谢谢。但抱歉,这个问题不是关于如何基准测试GC算法的。 - cruftex
你询问了关于“如何基准测试内存使用情况”和“内存使用效率”的问题,在Java中,内存使用与GC性能直接相关。 - Aleš
1
也许存在一些误解。这个问题并不是关于速度的。基准测试是关于比较性能指标的。这可以是运行时间、吞吐量、使用的内存等等。要计算所使用的(静态)内存量,您还可以迭代所有堆对象并累加其大小。 - cruftex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接