Skip to content

📈 Benchmark 001: Method Calls

📘 Preface

Method calls in Java aren't just about syntax, how a method is invoked at the JVM level can drastically affect performance. This benchmark explores multiple ways to invoke a method: directly, through reflection, and using MethodHandles, including the more performant invokeExact. These techniques are useful for understanding bytecode generation, runtime dispatch, and how the JVM optimises method calls under different conditions.

🔬 Test Case

java
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class BM_001_MethodCall {

  public static class TestClass {

    static final TestClass INSTANCE = new TestClass();

    static final Method STATIC_METHOD, METHOD;
    static final MethodHandle STATIC_METHOD_HANDLE, METHOD_HANDLE;

    static {
      try {
        STATIC_METHOD = TestClass.class.getMethod("staticMethod");
        STATIC_METHOD_HANDLE = MethodHandles.lookup().unreflect(STATIC_METHOD);
        METHOD = TestClass.class.getMethod("method");
        METHOD_HANDLE = MethodHandles.lookup().unreflect(METHOD);
      } catch (ReflectiveOperationException e) {
        throw new RuntimeException(e);
      }
    }

    public static int staticMethod() {
      return 42;
    }

    public int method() {
      return 42;
    }
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void staticMethod(Blackhole bh) {
    bh.consume(TestClass.staticMethod());
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void staticMethodReflection(Blackhole bh)
      throws InvocationTargetException, IllegalAccessException {
    bh.consume(TestClass.STATIC_METHOD.invoke(null));
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void staticMethodMH(Blackhole bh) throws Throwable {
    bh.consume(TestClass.STATIC_METHOD_HANDLE.invoke());
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void staticMethodMHExact(Blackhole bh) throws Throwable {
    bh.consume((int) TestClass.STATIC_METHOD_HANDLE.invokeExact());
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void method(Blackhole bh) {
    bh.consume(TestClass.INSTANCE.method());
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void methodReflection(Blackhole bh)
      throws InvocationTargetException, IllegalAccessException {
    bh.consume(TestClass.METHOD.invoke(TestClass.INSTANCE));
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void methodMH(Blackhole bh) throws Throwable {
    bh.consume(TestClass.METHOD_HANDLE.invoke(TestClass.INSTANCE));
  }

  @Benchmark
  @OperationsPerInvocation(1000)
  public void methodMHExact(Blackhole bh) throws Throwable {
    bh.consume((int) TestClass.METHOD_HANDLE.invokeExact(TestClass.INSTANCE));
  }
}

✅ Results

Benchmark                                 Mode  Cnt   Score    Error  Units
BM_001_MethodCall.method                  avgt    5  ≈ 10⁻³           ns/op
BM_001_MethodCall.methodMH                avgt    5   0.003 ±  0.001  ns/op
BM_001_MethodCall.methodMHExact           avgt    5  ≈ 10⁻³           ns/op
BM_001_MethodCall.methodReflection        avgt    5   0.003 ±  0.001  ns/op
BM_001_MethodCall.staticMethod            avgt    5  ≈ 10⁻³           ns/op
BM_001_MethodCall.staticMethodMH          avgt    5   0.003 ±  0.001  ns/op
BM_001_MethodCall.staticMethodMHExact     avgt    5  ≈ 10⁻³           ns/op
BM_001_MethodCall.staticMethodReflection  avgt    5   0.003 ±  0.001  ns/op

🔎 Findings

We can see that MethodHandles, when stored in a static final variable can be inlined to basically the same as a direct method call. Reflection still lags behind a little.