Java单元测试:如何测量方法调用的内存占用



假设我有一个类要做一些繁重的处理,要处理几个集合。我想做的是确保这样的操作不会导致内存不足,甚至更好。我想设置一个可以使用多少内存的阈值。

class MyClass()
{
   public void myMethod()
   {
      for(int i=0; i<10000000; i++)
      {
         // Allocate some memory, may be several collections
      }
   }
}
class MyClassTest
{
   @Test
   public void myMethod_makeSureMemoryFootprintIsNotBiggerThanMax()
   {
      new MyClass().myMethod(); 
      // How do I measure amount of memory it may try to allocate?
   }
}

做这件事的正确方法是什么?或者这不可能/不可行?

我能想到几个选项:

  • 通过微基准标记(即jmh(来了解方法需要多少内存
  • 建立基于启发式估计的分配策略。有几种开源解决方案实现类大小估计,即ClassSize。一个更简单的方法可以是利用缓存来释放很少使用的对象(即Guava的缓存(。正如@EnnoShioji所提到的,Guava的缓存有基于内存的驱逐策略

您还可以编写自己的基准测试来计算内存。想法是

  1. 让一个线程运行
  2. 创建一个新数组来存储要分配的对象。所以在GC运行期间不会收集这些对象
  3. System.gc()memoryBefore = runtime.totalMemory() - runtime.freeMemory()
  4. 分配对象。将它们放入阵列中
  5. System.gc()memoryAfter = runtime.totalMemory() - runtime.freeMemory()

这是我在轻量级微基准测试工具中使用的一种技术,该工具能够以字节精度测量内存分配。

您可以使用探查器(例如JProfiler(按类查看内存使用情况。或者,正如提到的Areo,只是打印内存使用:

    Runtime runtime = Runtime.getRuntime();
    long usedMemoryBefore = runtime.totalMemory() - runtime.freeMemory();
    System.out.println("Used Memory before" + usedMemoryBefore);
        // working code here
    long usedMemoryAfter = runtime.totalMemory() - runtime.freeMemory();
    System.out.println("Memory increased:" + (usedMemoryAfter-usedMemoryBefore));

要测量当前内存使用情况,请使用:

CCD_ 5,Runtime.getRuntime().totalMemory()

下面是一个很好的例子:获取操作系统级系统信息

但这种测量并不精确,但它可以给你很多信息。另一个问题是GC,它是不可预测的。

这里有一个来自Netty的例子,它做了类似的事情:MemorywareThreadPoolExecutor。Guava的缓存类也有一个基于大小的驱逐。你可以查看这些来源并复制他们正在做的事情。特别是,以下是Netty如何估计对象大小。本质上,您应该估计在该方法中生成的对象的大小,并进行计数。

获取总体内存信息(比如有多少堆可用/使用(将帮助您决定分配给方法的内存使用量,但不能跟踪单个方法调用使用了多少内存。

话虽如此,你确实需要这个是非常罕见的。在大多数情况下,通过限制给定点上可以有多少对象来限制内存使用量(例如,通过使用有界队列(就足够了,而且实现起来要简单得多。

这个问题有点棘手,因为Java可以在处理过程中分配大量短命对象,这些对象随后将在垃圾收集过程中被收集。在公认的答案中,我们不能肯定地说垃圾收集已经在任何给定的时间运行。即使我们引入了循环结构,使用多个System.gc()调用,垃圾回收也可能在方法调用之间运行。

一个更好的方法是使用中建议的一些变体https://cruftex.net/2017/03/28/The-6-Memory-Metrics-You-Should-Track-in-Your-Java-Benchmarks.html,其中System.gc()被触发,但我们也等待报告的GC计数增加:

long getGcCount() {
    long sum = 0;
    for (GarbageCollectorMXBean b : ManagementFactory.getGarbageCollectorMXBeans()) {
        long count = b.getCollectionCount();
        if (count != -1) { sum += count; }
    }
    return sum;
}
long getReallyUsedMemory() {
    long before = getGcCount();
    System.gc();
    while (getGcCount() == before);
    return getCurrentlyAllocatedMemory();
}
long getCurrentlyAllocatedMemory() {
    final Runtime runtime = Runtime.getRuntime();
    return (runtime.totalMemory() - runtime.freeMemory()) / (1024 * 1024);
}

这仍然只提供代码在给定时间实际分配的内存的近似值,但该值通常更接近人们通常感兴趣的值。由于GC可以在进程运行时随时触发,因此它将每秒记录内存使用情况,并报告使用的最大内存。

runnable是需要测量的实际过程,runTimeSecs是过程运行的预期时间。这是为了确保线程计算内存不会在实际进程之前终止。

public void recordMemoryUsage(Runnable runnable, int runTimeSecs) {
    try {
        CompletableFuture<Void> mainProcessFuture = CompletableFuture.runAsync(runnable);
        CompletableFuture<Void> memUsageFuture = CompletableFuture.runAsync(() -> {

            long mem = 0;
            for (int cnt = 0; cnt < runTimeSecs; cnt++) {
                long memUsed = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
                mem = memUsed > mem ? memUsed : mem;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ;
            System.out.println("Max memory used (gb): " + mem/1000000000D);
        });
        CompletableFuture<Void> allOf = CompletableFuture.allOf(mainProcessFuture, memUsageFuture);
        allOf.get();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

估计内存使用情况的最简单方法是使用Runtime类中的方法

我建议不要依赖它,而只用于近似估计。理想情况下,您应该只记录这些信息并自行分析,而不要将其用于测试或代码的自动化

也许它不是很可靠,但在像单元测试这样的封闭环境中,它可能会给你接近现实的估计
特别是不能保证调用System.gc()垃圾收集器后会在我们期望的时候运行(这只是GC的一个建议(,这里描述的freeMemory方法存在精度限制:https://stackoverflow.com/a/17376879/1673775而且可能还有更多的注意事项。

解决方案:

private static final long BYTE_TO_MB_CONVERSION_VALUE = 1024 * 1024;
@Test
public void memoryUsageTest() {
  long memoryUsageBeforeLoadingData = getCurrentlyUsedMemory();
  log.debug("Used memory before loading some data: " + memoryUsageBeforeLoadingData + " MB");
  List<SomeObject> somethingBigLoadedFromDatabase = loadSomethingBigFromDatabase();
  long memoryUsageAfterLoadingData = getCurrentlyUsedMemory();
  log.debug("Used memory after loading some data: " + memoryUsageAfterLoadingData + " MB");
  log.debug("Difference: " + (memoryUsageAfterLoadingData - memoryUsageBeforeLoadingData) + " MB");
  someOperations(somethingBigLoadedFromDatabase);
}
private long getCurrentlyUsedMemory() {
  System.gc();
  return (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / BYTE_TO_MB_CONVERSION_VALUE;
}

许多其他答案都警告GC是不可预测的。然而,从Java11开始,Epsilon垃圾收集器就包含在JVM中,JVM不执行GC。

指定以下命令行选项以启用它:

-XX:+UnlockExperimentalVMOptions 
-XX:+UseEpsilonGC

然后可以确保垃圾收集不会干扰内存计算。

最新更新