📈 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.