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

332nm8kg  于 2023-04-06  发布在  Java
关注(0)|答案(8)|浏览(344)

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

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?
   }
}

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

l3zydbqr

l3zydbqr1#

我可以想到几个选择:

  • 通过微基准测试(即jmh)找出你的方法需要多少内存。
  • 基于启发式估计构建分配策略。有几个开源解决方案实现了类大小估计,例如ClassSize。一个更简单的方法是使用一个缓存来释放很少使用的对象(例如Guava的缓存)。正如@EnnoShioji提到的,Guava的缓存有基于内存的回收策略。

您也可以编写自己的基准测试来计算内存。
1.只有一个线程在运行。
1.创建一个新的数组来存储要分配的对象。这样在GC运行期间这些对象就不会被收集。
1.一个月一次,一个月一次
1.分配你的对象。把它们放到数组中。

  1. System.gc()memoryAfter = runtime.totalMemory() - runtime.freeMemory()
    这是我在lightweight micro-benchmark tool中使用的一种技术,它能够以字节精度测量内存分配。
owfi6suc

owfi6suc2#

你可以使用profiler(例如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));
mwecs4sa

mwecs4sa3#

要测量当前内存使用情况,请使用用途:
一个月一次,一个月一次
下面是一个很好的例子:get OS-level system information
但是这种测量并不精确,但它可以给予你很多信息。另一个问题是GC,它是不可预测的。

b1payxdu

b1payxdu4#

下面是Netty的一个例子,它做了类似的事情:MemoryAwareThreadPoolExecutor. Guava的cache class也有一个基于大小的回收。你可以看看这些源代码并复制它们的操作。特别是,这里是Netty如何估计对象大小的。本质上,你应该估计你在方法中生成的对象的大小并进行计数。
获取总体内存信息(例如可用/使用了多少堆)将帮助您决定分配给方法的内存使用量,但不能跟踪单个方法调用使用了多少内存。
在大多数情况下,通过限制在给定点可以有多少对象来限制内存使用(例如,通过使用有界队列)已经足够好了,而且实现起来要简单得多。

ie3xauqp

ie3xauqp5#

这个问题有点棘手,因为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);
}

这仍然只给出了代码在给定时间实际分配的内存的近似值,但该值通常更接近人们通常感兴趣的值。

xeufq47z

xeufq47z6#

下面是一个在单独的线程中运行内存使用的示例代码。由于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();
    }
}
czq61nw1

czq61nw17#

估计内存使用量最简单的方法是使用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;
}
2w2cym1i

2w2cym1i8#

许多其他的答案都警告说GC是不可预测的。然而,从Java 11开始,Epsilon garbage collector已经包含在JVM中,它不执行GC。
指定以下命令行选项以启用它:

-XX:+UnlockExperimentalVMOptions 
-XX:+UseEpsilonGC

这样就可以确保垃圾回收不会干扰内存计算。

相关问题