【问题标题】:Java unit testing: how to measure memory footprint for method callJava单元测试:如何测量方法调用的内存占用
【发布时间】:2013-11-16 02:36:27
【问题描述】:

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

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

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

【问题讨论】:

  • @Steve P.:获取整体内存使用情况不会让您知道内存用于什么。
  • 是的,但我可以设置“此算法不能消耗超过 100KB 的 RAM,不应依赖于要处理的数据大小”之类的要求。想法是通过创建显式单元测试来强制执行此操作。
  • 但是为什么要设置这样的要求呢?资源很便宜,Java 就是针对这一事实而设计的。在遇到实际问题之前,您不应该担心内存消耗。除此之外,即使你找到了一种测量记忆的方法,你仍然无法测量你是否正确地解释了结果,并且你创造了一个真实的工作环境来产生真实的结果。你只会得到“一个数字”,并不会真正变得更聪明。
  • @Gimby 如果一些好奇的开发人员试图测量两种方法会有什么危害——在有意义之前它是否必须在现实的工作环境中——我猜不是。通常,某些方法在任何情况下都比其他方法占用更少的内存/ CPU 或两者。这样的分析可以派上用场,至少可以得到一个粗略的想法。
  • @Gimby “资源很便宜”当您不知道解决方案的基础设施和人员预算时,这是一个相当冒昧的评论

标签: java unit-testing junit out-of-memory testng


【解决方案1】:

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

【讨论】:

  • 这不是很准确,因为 System.gc() 只是一个“请求”,不会当场发生。你永远不知道 gc 什么时候真正会运行。
  • @R.A 是的,你是对的。我在答案中写了它。这种方法很简单,不需要任何额外的工具,但不幸的是它可能根本不准确。我有一个案例,它看起来很有效,但这是一个非常简单的应用程序,我的发现完全基于我的观察,没有证据证明它可以正常工作。
【解决方案2】:

这是在单独的线程中运行内存使用的示例代码。由于在进程运行的任何时候都可以触发 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();
    }
}

【讨论】:

    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

      您可以使用分析器(例如 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));
      

      【讨论】:

      • 给更多读者的小提示:值以字节为单位,除以 1000000 得到以 MB 为单位的值。
      • @pasha701 使用这种方法的小问题,runtime.totalMemory() 在该实例中分配给 jvm 的内存。这可能会在执行工作代码后增加超时。在那种情况下(usedMemoryAfter-usedMemoryBefore)在某些情况下甚至可能给出负值。
      【解决方案5】:

      我能想到几个选项:

      • 通过微基准(即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()

      这是我在lightweight micro-benchmark tool 中使用的一种技术,它能够以字节精度测量内存分配。

      【讨论】:

      • 自定义方式仍然是一个近似值。即使在之前和之后运行 GC,也有可能在这两个调用之间调用年轻 GC(特别是如果测试的调用是内存密集型的),即使您将所有 RAM 分配为堆。此外,还有不同的 JVM 和不同的 GC(例如,G1 与 mark & sweep 非常不同)所以我不知道结果有多可靠......我喜欢自定义 GC 的想法。如果 JMH 可以做到的话,最好用它(如你所说)。
      • 实际上,在分配内存之前多次调用 System.gc() 会很有帮助。很明显,确切的行为还取决于 JVM 和 GC。
      【解决方案6】:

      这是来自 Netty 的一个例子,它做了类似的事情:MemoryAwareThreadPoolExecutor。 Guava 的cache class 也有一个基于大小的驱逐。您可以查看这些来源并复制他们正在做的事情。特别是,Netty 是这样的estimating object sizes。本质上,您会估计在方法中生成的对象的大小并进行计数。

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

      话虽如此,您很少有合法需要这个。在大多数情况下,通过限制给定点可以有多少对象(例如通过使用有界队列)来限制内存使用量就足够了,而且实现起来要简单得多。

      【讨论】:

        【解决方案7】:

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

        Runtime.getRuntime().freeMemory() , Runtime.getRuntime().totalMemory()

        这是一个很好的例子: get OS-level system information

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2023-03-06
          • 1970-01-01
          • 2016-12-06
          • 2010-09-23
          相关资源
          最近更新 更多