当前位置: 首页 > 知识库问答 >
问题:

带JMH和Mode.AverageTime的OutOfMemory

鲁炳
2023-03-14

我正在编写一个微基准,用操作符和StringBuilder比较String连接。为此,我基于使用batchSize参数的OpenJDK示例创建了一个JMH基准类:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {

    private String string;

    private StringBuilder stringBuilder;

    @Setup(Level.Iteration)
    public void setup() {
        string = "";
        stringBuilder = new StringBuilder();
    }

    @Benchmark
    public void stringConcatenation() {
        string += "some more data";
    }

    @Benchmark
    public void stringBuilderConcatenation() {
        stringBuilder.append("some more data");
    }

}

当我运行基准测试时,我得到< code > stringBuilderConcatenation 方法的以下错误:

java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3332)
    at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)
    at java.lang.StringBuilder.append(StringBuilder.java:136)
    at link.pellegrino.string_concatenation.StringConcatenationBenchmark.stringBuilderConcatenation(StringConcatenationBenchmark.java:29)
    at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_avgt_jmhStub(StringConcatenationBenchmark_stringBuilderConcatenation.java:165)
    at link.pellegrino.string_concatenation.generated.StringConcatenationBenchmark_stringBuilderConcatenation.stringBuilderConcatenation_AverageTime(StringConcatenationBenchmark_stringBuilderConcatenation.java:130)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:430)
    at org.openjdk.jmh.runner.BenchmarkHandler$BenchmarkTask.call(BenchmarkHandler.java:412)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

我认为默认的JVM堆大小必须增加,所以我尝试使用JMH提供的-jvmArgs选项,使用-Xmx10Gvalue来允许10GB的内存。不幸的是,我仍然得到错误。

因此,我试图将批处理大小参数的值减小到 1,但我仍然得到一个内存不足错误。

我找到的唯一解决方法是将基准模式设置为mode.SingleShotTime。由于此模式似乎将批处理视为单个快照(即使“单位”列中显示s/op),因此我似乎得到了我想要的度量:执行批处理操作集的平均时间。然而,我仍然不明白为什么它不能与Mode.AverageTime一起工作。

另请注意,无论使用基准测试模式,方法字符串的基准测试都会按预期工作。此问题仅发生在使用字符串生成器的字符串生成器连接方法中。

任何有助于理解为什么前面的示例不能使用设置为mode的Benchmark模式。欢迎使用AverageTime

我使用的JMH版本是1.10.4。


共有1个答案

郎华皓
2023-03-14

你是对的,Mode.SingleShotTime就是你需要的:它测量单个批处理的时间。当使用Mode.AverageTime时,你的迭代仍然工作,直到迭代时间结束(默认为1秒)。它测量每次执行单个批处理的时间(只计算在执行时间内完全完成的批次),因此最终结果不同,但执行时间是相同的。

另一个问题是@Setup(Level.Iteration)强制在每次迭代之前执行设置,而不是在每次批处理之前。因此,字符串实际上不受批大小的限制。字符串版本不会导致OutOfMemoryError,因为它比StringBuilder慢得多,所以在1秒内它能够构建更短的字符串。

修复基准测试的方法不是很漂亮(同时仍使用平均时间模式和batchSize参数)是手动重置字符串/stringBuilder:

@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Measurement(batchSize = 10000, iterations = 10)
@Warmup(batchSize = 10000, iterations = 10)
@Fork(1)
public class StringConcatenationBenchmark {
    private static final String S = "some more data";
    private static final int maxLen = S.length()*10000;

    private String string;

    private StringBuilder stringBuilder;

    @Setup(Level.Iteration)
    public void setup() {
        string = "";
        stringBuilder = new StringBuilder();
    }

    @Benchmark
    public void stringConcatenation() {
        if(string.length() >= maxLen) string = "";
        string += S;
    }

    @Benchmark
    public void stringBuilderConcatenation() {
        if(stringBuilder.length() >= maxLen) stringBuilder = new StringBuilder();
        stringBuilder.append(S);
    }
}

这是我的盒子上的结果(i5 3340,4Gb内存,64位Win7,JDK 1.80_45):

Benchmark                   Mode  Cnt       Score       Error  Units
stringBuilderConcatenation  avgt   10     145.997 ±     2.301  us/op
stringConcatenation         avgt   10  324878.341 ± 39824.738  us/op

因此,您可以看到,对于字符串连接1e6 / 324878),只有大约3个批次适合第二个批次,而对于字符串生成器连接,可以执行数千个批次,从而导致导致 OutOfMemoryError 的巨大字符串。

我不知道为什么添加更多内存对您不起作用,对我来说-Xmx4G足以运行原始基准的字符串生成器测试。可能你的盒子更快,所以得到的字符串更长。请注意,对于非常大的字符串,即使您有足够的内存,也可以达到数组大小限制(20亿个元素)。添加内存后检查异常堆栈跟踪:它是相同的吗?如果达到数组大小限制,它仍将是内存不足错误,但堆栈跟踪会略有不同。无论如何,即使有足够的内存,基准测试结果也将是不正确的(对于字符串字符串生成器)。

 类似资料:
  • 我一直在研究jmh实现“多线程”基准测试的方式。根据我的理解,和注释允许并行运行同一组中的基准测试。 使用CPU监视器,我得到两个CPU已完全使用。我想知道跑步者是如何解释这些注释的。

  • 问题内容: 我在这里看到了很多线程,它们可以进行比较并尝试回答哪个更快:或。 查看源代码,似乎应该 慢很多 ,我的意思是它进行了许多安全检查并使用了反射。我决定进行测量,首先运行jdk-8。这是使用的代码。 我认为这里没有什么大的惊喜(JIT进行了很多优化,使这种差异不 那么大 ): 热代码的差异约为 2 倍,单次射击时间的差异更糟。 现在,我切换到jdk-9(以防万一,请构建157)并运行相同的

  • 我正在使用JMH对自定义集合实现进行性能测试。 我想模仿一个场景,其中读取次数比写入次数大10倍。 我使用了这个非对称基准测试示例,并创建了一个包含10个读取线程和1个写入线程的组: 我使用参数运行测试。在报告中,变量对所有基准测试都是相同的: 这是否意味着实验中的读取次数等于写入次数?如果有,如何正确实施?更一般的问题是:我在这个设置中遗漏了什么吗?

  • 如果IIUC每个fork都会创建一个单独的虚拟机,因为每个虚拟机实例可能在JIT指令中略有不同? 我也很好奇时间属性在下面的注释中有什么作用: 蒂亚,奥莱

  • 我想写一个基准来跟踪执行一些Java代码所需的最大、最小和平均时间。 我读了关于JMH(JavaMicro基准线束),它似乎符合我的需求,但我不太确定我应该使用哪种模式: > 通过在有时限的迭代中连续调用基准方法并计算我们执行该方法的次数来测量原始吞吐量。 这似乎不是我想要的。 测量执行基准测试方法所需的平均时间。 这应该是我需求的三分之一。 衡量单个基准方法执行运行所需的时间。 这似乎不是我想要

  • 为了避免JVM上大多数众所周知的基准测试陷阱,我使用了jmh(java微基准测试工具)。此外,我确实知道垃圾回收机制会扭曲基准测试结果。 现在我想知道用< code>-prof gc的jmh的结果有多有表现力。jmh是否采取特殊措施使结果有意义? 我脑海中的其他问题是: 如果在测量期间没有进行垃圾收集,该怎么办 如果分析gc,jmh会强制垃圾收集吗 在单独的JVM/java进程中运行的每个基准对g