VarHandle::getAcquire为何开销巨大?能否替代volatile优化延迟?
VarHandle::getAcquire 高开销原因及与volatile的替代可行性分析
背景:C++与Java的内存语义对比
在x86平台上,C/C++的stdatomic提供release-acquire语义,得益于x86的内存模型,该语义无需额外开销。使用atomic_load_explicit(&i, memory_order_acquire);和atomic_store_explicit(&i, 0, memory_order_release);的核心价值在于,x86平台无需额外内存屏障指令就能保证正确的release-acquire语义。
Java中的VarHandle::getAcquire和VarHandle::setRelease提供大致相同的内存语义,但测试发现VarHandle::getAcquire会引入大量额外开销,完全抵消了弱内存语义带来的优化价值。
基准测试代码
用于测试的JMH基准代码如下:
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(value = 1) @BenchmarkMode(org.openjdk.jmh.annotations.Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) @State(Scope.Benchmark) public class Benchmarks { private int variable = 0; private static VarHandle VAR_HANDLE; static { try { MethodHandles.Lookup l = MethodHandles.lookup(); VAR_HANDLE = l.findVarHandle(Benchmarks.class, "variable", int.class); } catch (ReflectiveOperationException e) { throw new Error(e); } } @Benchmark @BenchmarkMode(Mode.AverageTime) public void readAcquire(Blackhole bh) { bh.consume(VAR_HANDLE.getAcquire(this)); } }
性能分析:额外操作来源
通过prof perfasm输出可以看到大量额外操作,例如:
mov 0x20(%r12,%r8,8),%r9d ;*aaload {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.VarHandle::getMethodHandle@10 (line 1979) ; - java.lang.invoke.VarHandleGuards::guard_L_L@50 (line 40) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) mov 0x10(%r12,%r9,8),%ecx ;*getfield type {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.MethodHandle::asType@2 (line 839) ; - java.lang.invoke.VarHandleGuards::guard_L_L@59 (line 41) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) ; implicit exception: dispatches to 0x00007f50282ef40c cmp $0xe2d7411d,%ecx ; {oop(a 'java/lang/invoke/MethodType'{0x0000000716ba08e8} = (Ljava/lang/invoke/VarHandle;Lcom/test/Benchmarks;)Ljava/lang/Object;)} je 0x7f50282ef318 ;*if_acmpne {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.MethodHandle::asType@5 (line 839) ; - java.lang.invoke.VarHandleGuards::guard_L_L@59 (line 41) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) mov 0x18(%r12,%r9,8),%r10d ;*getfield asTypeCache {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.MethodHandle::asTypeCached@1 (line 851) ; - java.lang.invoke.MethodHandle::asType@12 (line 843) ; - java.lang.invoke.VarHandleGuards::guard_L_L@59 (line 41) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) mov 0x10(%r12,%r10,8),%r8d ;*getfield type {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.MethodHandle::asTypeCached@11 (line 852) ; - java.lang.invoke.MethodHandle::asType@12 (line 843) ; - java.lang.invoke.VarHandleGuards::guard_L_L@59 (line 41) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) ; implicit exception: dispatches to 0x00007f50282ef428 cmp $0xe2d7411d,%r8d ; {oop(a 'java/lang/invoke/MethodType'{0x0000000716ba08e8} = (Ljava/lang/invoke/VarHandle;Lcom/test/Benchmarks;)Ljava/lang/Object;)} jne 0x7f50282ef2e4 ;*if_acmpne {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.MethodHandle::asTypeCached@14 (line 852) ; - java.lang.invoke.MethodHandle::asType@12 (line 843) ; - java.lang.invoke.VarHandleGuards::guard_L_L@59 (line 41) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) lea (%r12,%r11,8),%rdx ;*getstatic VAR_HANDLE {reexecute=0 rethrow=0 return_oop=0} ; - com.test.Benchmarks::readAcquire@1 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190) lea (%r12,%r10,8),%rsi ;*getfield asTypeCache {reexecute=0 rethrow=0 return_oop=0} ; - java.lang.invoke.MethodHandle::asTypeCached@1 (line 851) ; - java.lang.invoke.MethodHandle::asType@12 (line 843) ; - java.lang.invoke.VarHandleGuards::guard_L_L@59 (line 41) ; - com.test.Benchmarks::readAcquire@5 (line 33) ; - com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190)
还有动态函数调用的开销:
callq 0x7fa21883fd80 ;*invokevirtual invokeBasic {reexecute=0 rethrow=0 return_oop=1} ;java.lang.invoke.VarHandleGuards::guard_L_L@64 (line 41) ;com.test.Benchmarks::readAcquire@5 (line 32) ;com.test.jmh_generated.Benchmarks_readAcquire_jmhTest::readAcquire_avgt_jmhStub@17 (line 190)
这些额外操作来自VarHandleGuards::guard_L_L方法,该方法会在每次VarHandle调用时被隐式执行,代码如下:
static final Object guard__L(VarHandle handle, VarHandle.AccessDescriptor ad) throws Throwable { handle.checkExactAccessMode(ad); if (handle.isDirect() && handle.vform.methodType_table[ad.type] == ad.symbolicMethodTypeErased) { Object r = MethodHandle.linkToStatic(handle, handle.vform.getMemberName(ad.mode)); return ad.returnType.cast(r); } else { MethodHandle mh = handle.getMethodHandle(ad.mode); return mh.asType(ad.symbolicMethodTypeInvoker).invokeBasic(handle.asDirect()); } }
Java volatile的内存语义与开销
Java中volatile提供顺序一致性保证:
- 在x86平台上,读取
volatile变量无需额外内存屏障; - 写入
volatile变量通常会在内存写入指令后插入lock addl $0x0, (%rsp)指令(类似C++中seq_cst存储用xchg而非单独的mfence或lock指令,成本略低,但全屏障是主要开销来源)。
问题解答
1. 为什么VarHandle::getAcquire会引入大量额外操作?
核心原因是VarHandle的调用路径包含了运行时合法性检查、方法句柄类型适配与动态调用逻辑:
- 每次调用都会通过
VarHandleGuards::guard_L_L执行checkExactAccessMode,校验访问模式的合法性; - 即使是direct类型的VarHandle,也要检查方法类型是否匹配;
- 若类型不匹配或非direct VarHandle,会触发
MethodHandle.asType进行类型适配,还会调用invokeBasic执行动态方法调用,这些步骤带来了大量内存访问、分支判断和函数调用开销,远超过内存语义本身的成本。
2. 能否用VarHandle::getAcquire替代volatile以优化延迟?
在当前JVM实现下,直接用这种方式替代volatile无法实现延迟优化:
- 虽然
getAcquire的内存语义比volatile更弱(仅acquire语义,而非顺序一致性),理论上x86平台下内存屏障开销更低,但VarHandle调用路径带来的额外开销远大于volatile读的开销(x86上volatile读几乎无额外屏障); - 若要利用VarHandle的弱内存语义优化,需要确保JIT编译器充分优化VarHandle调用:比如保证
VarHandle为static final(测试代码已满足),并且在热点代码中让JIT内联掉guard逻辑; - 对于写操作,
setRelease的release语义在x86下无需额外屏障,若能优化VarHandle的调用路径,setRelease可能比volatile写更高效,但读场景下当前VarHandle的开销完全抵消了弱语义的优势。
内容的提问来源于stack exchange,提问作者Some Name
相关产品推荐
相关产品推荐

