假设我有一个类要做一些繁重的处理,要处理几个集合。我想做的是确保这样的操作不会导致内存不足,甚至更好。我想设置一个可以使用多少内存的阈值。
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的缓存有基于内存的驱逐策略
您还可以编写自己的基准测试来计算内存。想法是
- 让一个线程运行
- 创建一个新数组来存储要分配的对象。所以在GC运行期间不会收集这些对象
System.gc()
、memoryBefore = runtime.totalMemory() - runtime.freeMemory()
- 分配对象。将它们放入阵列中
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
然后可以确保垃圾收集不会干扰内存计算。