从Hotspot源码分析volatile的实现原理
本人开源了重试利器 Attempt 感兴趣的可以去 clone下代码阅读,也欢迎 start 和 discuss。gitHub 搜索:IceFrozen/Attempt
0
概诉
1
volatile 应用场景
01
可见性
02
有序性
下面我们来看有序性。
运行结果如下:
执行次数:247521,x:0 y:0
看第二个例子:
2
原理
1、 指令重排序
2、 MESI协议的导致的缓存未刷新。
01
指令重排序
02
内存屏障
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
在每个volatile写操作的前面插入一个StoreStore屏障。
在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。
在每个volatile读操作的后面插入一个LoadStore屏障。
总结来说,volatile 可见性包括两个方面:
写入的 volatile 变量在写完之后能被别的 CPU 在下一次读取中读取到;
写入 volatile 变量之前的操作在别的 CPU 看到 volatile 的最新值后一定也能被看到;
对于第一个方面,主要通过:
读取 volatile 变量不能使用寄存器,每次读取都要去内存拿;
禁止读 volatile 变量后续操作被重排到读 volatile 之前;
03
x86平台下的内存屏障
2
HotSpot屏障的实现
01
字节码上的区别
上述代码编译后,我们用javap命令来看一下字节码:
略volatile int val;descriptor: Iflags: ACC_VOLATILEint val2;descriptor: Iflags:略public int add();descriptor: ()Iflags: ACC_PUBLICCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #2 // Field val:I5: iconst_16: iadd7: putfield #2 // Field val:I10: aload_011: getfield #2 // Field val:I14: ireturnLineNumberTable:line 7: 0line 8: 10public int add2();descriptor: ()Iflags: ACC_PUBLICCode:stack=3, locals=1, args_size=10: aload_01: dup2: getfield #3 // Field val2:I5: iconst_16: iadd7: putfield #3 // Field val2:I10: aload_011: getfield #3 // Field val2:I14: ireturnLineNumberTable:line 11: 0line 12: 10}
02
读操作
void TemplateTable::getfield(int byte_no) {getfield_or_static(byte_no, false);}
void TemplateTable::getfield_or_static(int byte_no, bool is_static, RewriteControl rc) {transition(vtos, vtos);const Register cache = rcx;const Register index = rdx;const Register obj = c_rarg3;const Register off = rbx;const Register flags = rax;const Register bc = c_rarg3; // uses same reg as obj, so don't mix them//给该字段创建一个ConstantPoolCacheEntry,该类表示常量池中某个方法或者字段的解析结果resolve_cache_and_index(byte_no, cache, index, sizeof(u2));//发布jvmti事件jvmti_post_field_access(cache, index, is_static, false);//加载该字段的偏移量,flags,如果是静态字段还需要解析该类class实例对应的oopload_field_cp_cache_entry(obj, cache, index, off, flags, is_static);if (!is_static) {//将被读取属性的oop放入obj中pop_and_check_object(obj);}const Address field(obj, off, Address::times_1);Label Done, notByte, notBool, notInt, notShort, notChar,notLong, notFloat, notObj, notDouble;__ shrl(flags, ConstantPoolCacheEntry::tos_state_shift);// Make sure we don't need to mask edx after the above shiftassert(btos == 0, "change code, btos != 0");__ andl(flags, ConstantPoolCacheEntry::tos_state_mask);//判断是否是byte类型__ jcc(Assembler::notZero, notByte);//读取该属性,并放入rax中__ load_signed_byte(rax, field);__ push(btos);if (!is_static) {//将该指令改写成_fast_bgetfield,下一次执行时就是_fast_bgetfieldpatch_bytecode(Bytecodes::_fast_bgetfield, bc, rbx);}//跳转到Done__ jmp(Done);__ bind(notByte);//判断是否boolean类型__ cmpl(flags, ztos);__ jcc(Assembler::notEqual, notBool);// ztos (same code as btos)__ load_signed_byte(rax, field);__ push(ztos);// Rewrite bytecode to be fasterif (!is_static) {// use btos rewriting, no truncating to t/f bit is needed for getfield.patch_bytecode(Bytecodes::_fast_bgetfield, bc, rbx);}__ jmp(Done);__ bind(notBool);//判断是否引用类型__ cmpl(flags, atos);__ jcc(Assembler::notEqual, notObj);// atos__ load_heap_oop(rax, field);__ push(atos);if (!is_static) {patch_bytecode(Bytecodes::_fast_agetfield, bc, rbx);}__ jmp(Done);__ bind(notObj);//判断是否int类型__ cmpl(flags, itos);__ jcc(Assembler::notEqual, notInt);// itos__ movl(rax, field);__ push(itos);// Rewrite bytecode to be fasterif (!is_static) {patch_bytecode(Bytecodes::_fast_igetfield, bc, rbx);}__ jmp(Done);__ bind(notInt);//判断是否char类型__ cmpl(flags, ctos);__ jcc(Assembler::notEqual, notChar);// ctos__ load_unsigned_short(rax, field);__ push(ctos);// Rewrite bytecode to be fasterif (!is_static) {patch_bytecode(Bytecodes::_fast_cgetfield, bc, rbx);}__ jmp(Done);__ bind(notChar);//判断是否short类型__ cmpl(flags, stos);__ jcc(Assembler::notEqual, notShort);// stos__ load_signed_short(rax, field);__ push(stos);// Rewrite bytecode to be fasterif (!is_static) {patch_bytecode(Bytecodes::_fast_sgetfield, bc, rbx);}__ jmp(Done);__ bind(notShort);//判断是否long类型__ cmpl(flags, ltos);__ jcc(Assembler::notEqual, notLong);// ltos__ movq(rax, field);__ push(ltos);// Rewrite bytecode to be fasterif (!is_static) {patch_bytecode(Bytecodes::_fast_lgetfield, bc, rbx);}__ jmp(Done);__ bind(notLong);//判断是否float类型__ cmpl(flags, ftos);__ jcc(Assembler::notEqual, notFloat);// ftos__ movflt(xmm0, field);__ push(ftos);// Rewrite bytecode to be fasterif (!is_static) {patch_bytecode(Bytecodes::_fast_fgetfield, bc, rbx);}__ jmp(Done);__ bind(notFloat);// 只剩一种double类型__ movdbl(xmm0, field);__ push(dtos);// Rewrite bytecode to be fasterif (!is_static) {patch_bytecode(Bytecodes::_fast_dgetfield, bc, rbx);}__ bind(Done);// [jk] not needed currently// volatile_barrier(Assembler::Membar_mask_bits(Assembler::LoadLoad |// Assembler::LoadStore));
这里有几个概念解释一下:
void TemplateTable::fast_accessfield(TosState state) {transition(atos, state);//发布JVMTI事件if (JvmtiExport::can_post_field_access()) {// Check to see if a field access watch has been set before we// take the time to call into the VM.Label L1;__ mov32(rcx, ExternalAddress((address) JvmtiExport::get_field_access_count_addr()));__ testl(rcx, rcx);__ jcc(Assembler::zero, L1);// access constant pool cache entry__ get_cache_entry_pointer_at_bcp(c_rarg2, rcx, 1);__ verify_oop(rax);__ push_ptr(rax); // save object pointer before call_VM() clobbers it__ mov(c_rarg1, rax);// c_rarg1: object pointer copied above// c_rarg2: cache entry pointer__ call_VM(noreg,CAST_FROM_FN_PTR(address,InterpreterRuntime::post_field_access),c_rarg1, c_rarg2);__ pop_ptr(rax); // restore object pointer__ bind(L1);}//获取该字段对应的ConstantPoolCacheEntry__ get_cache_and_index_at_bcp(rcx, rbx, 1);//获取字段偏移量__ movptr(rbx, Address(rcx, rbx, Address::times_8,in_bytes(ConstantPoolCache::base_offset() +ConstantPoolCacheEntry::f2_offset())));//校验rax中实例对象oop,这里没有像getfield一样先把实例对象从栈顶pop到rax中,而是直接校验//这是因为fast_accessfield类指令的栈顶缓存类型是atos而不是vtos,即上一个指令执行完后会自动将待读取的实例放入rax中__ verify_oop(rax);__ null_check(rax);Address field(rax, rbx, Address::times_1);// access fieldswitch (bytecode()) {case Bytecodes::_fast_agetfield://将属性值拷贝到rax中__ load_heap_oop(rax, field);__ verify_oop(rax);break;case Bytecodes::_fast_lgetfield:__ movq(rax, field);break;case Bytecodes::_fast_igetfield:__ movl(rax, field);break;case Bytecodes::_fast_bgetfield:__ movsbl(rax, field);break;case Bytecodes::_fast_sgetfield:__ load_signed_short(rax, field);break;case Bytecodes::_fast_cgetfield:__ load_unsigned_short(rax, field);break;case Bytecodes::_fast_fgetfield:__ movflt(xmm0, field);break;case Bytecodes::_fast_dgetfield:__ movdbl(xmm0, field);break;default:ShouldNotReachHere();}}
我们看到,其实对于volatile变量来说,在读取的时候,并没有做处理。我们来看写操作。
03
写操作
void TemplateTable::putfield(int byte_no) {putfield_or_static(byte_no, false);}void TemplateTable::putstatic(int byte_no) {putfield_or_static(byte_no, true);}void TemplateTable::putfield_or_static(int byte_no, bool is_static) {transition(vtos, vtos);const Register cache = rcx;const Register index = rdx;const Register obj = rcx;const Register off = rbx;const Register flags = rax;const Register bc = c_rarg3;//找到该属性对应的ConstantPoolCacheEntryresolve_cache_and_index(byte_no, cache, index, sizeof(u2));//发布事件jvmti_post_field_mod(cache, index, is_static);//获取字段偏移量,flags,如果是静态属性获取对应类的class实例load_field_cp_cache_entry(obj, cache, index, off, flags, is_static);Label notVolatile, Done;__ movl(rdx, flags);__ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift);__ andl(rdx, 0x1);//.......// btos{//将栈顶的待写入值放入rax中__ pop(btos);//待写入的值pop出去后,如果是实例属性则栈顶元素为准备写入的实例//校验该实例是否为空,将其拷贝到obj寄存器中if (!is_static) pop_and_check_object(obj);//将rax中的待写入值写入到filed地址处__ movb(field, rax);if (!is_static) {//将该字节码改写成_fast_bputfield,下一次执行时直接执行_fast_bputfield,无需再次判断属性类型patch_bytecode(Bytecodes::_fast_bputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notByte);//判断是否boolean类型__ cmpl(flags, ztos);__ jcc(Assembler::notEqual, notBool);// ztos{__ pop(ztos);if (!is_static) pop_and_check_object(obj);__ andl(rax, 0x1);__ movb(field, rax);if (!is_static) {patch_bytecode(Bytecodes::_fast_zputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notBool);//判断是否引用类型__ cmpl(flags, atos);__ jcc(Assembler::notEqual, notObj);__ bind(notObj);//判断是否int类型__ cmpl(flags, itos);__ jcc(Assembler::notEqual, notInt);// itos{__ pop(itos);if (!is_static) pop_and_check_object(obj);__ movl(field, rax);if (!is_static) {patch_bytecode(Bytecodes::_fast_iputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notInt);//判断是否char类型__ cmpl(flags, ctos);__ jcc(Assembler::notEqual, notChar);// ctos{__ pop(ctos);if (!is_static) pop_and_check_object(obj);__ movw(field, rax);if (!is_static) {patch_bytecode(Bytecodes::_fast_cputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notChar);//判断是否short类型__ cmpl(flags, stos);__ jcc(Assembler::notEqual, notShort);// stos{__ pop(stos);if (!is_static) pop_and_check_object(obj);__ movw(field, rax);if (!is_static) {patch_bytecode(Bytecodes::_fast_sputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notShort);//判断是否long类型__ cmpl(flags, ltos);__ jcc(Assembler::notEqual, notLong);// ltos{__ pop(ltos);if (!is_static) pop_and_check_object(obj);__ movq(field, rax);if (!is_static) {patch_bytecode(Bytecodes::_fast_lputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notLong);//判断是否float类型__ cmpl(flags, ftos);__ jcc(Assembler::notEqual, notFloat);// ftos{__ pop(ftos);if (!is_static) pop_and_check_object(obj);__ movflt(field, xmm0);if (!is_static) {patch_bytecode(Bytecodes::_fast_fputfield, bc, rbx, true, byte_no);}__ jmp(Done);}__ bind(notFloat);// dtos{//只剩一个,double类型__ pop(dtos);if (!is_static) pop_and_check_object(obj);__ movdbl(field, xmm0);if (!is_static) {patch_bytecode(Bytecodes::_fast_dputfield, bc, rbx, true, byte_no);}}__ bind(Done);//判断是否volatile变量,如果不是则跳转到notVolatile__ testl(rdx, rdx);__ jcc(Assembler::zero, notVolatile);//如果是volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |Assembler::StoreStore));__ bind(notVolatile);}
同理,我们根据上面的template表,可以看到实际执行的代码段fast_storefield如下。
void TemplateTable::fast_storefield(TosState state) {transition(state, vtos);ByteSize base = ConstantPoolCache::base_offset();//发布jvmti时间jvmti_post_fast_field_mod();//获取该字段对应的ConstantPoolCacheEntry__ get_cache_and_index_at_bcp(rcx, rbx, 1);//获取该字段的flags__ movl(rdx, Address(rcx, rbx, Address::times_8,in_bytes(base +ConstantPoolCacheEntry::flags_offset())));//获取该字段的偏移量__ movptr(rbx, Address(rcx, rbx, Address::times_8,in_bytes(base + ConstantPoolCacheEntry::f2_offset())));Label notVolatile;__ shrl(rdx, ConstantPoolCacheEntry::is_volatile_shift);__ andl(rdx, 0x1);//将待写入的实例对象pop到rcx中,注意此处并没有像putfield一样把待写入的值先pop到rax中,//这是因为fast_storefield类的栈顶缓存类型不是vtos而是具体的写入值类型对应的类型,即上一个//字节码指令执行完成后会自动将待写入的值放入rax中pop_and_check_object(rcx);// field addressconst Address field(rcx, rbx, Address::times_1);// access fieldswitch (bytecode()) {case Bytecodes::_fast_aputfield:do_oop_store(_masm, field, rax, _bs->kind(), false);break;case Bytecodes::_fast_lputfield://将rax中的属性值写入到field地址__ movq(field, rax);break;case Bytecodes::_fast_iputfield:__ movl(field, rax);break;case Bytecodes::_fast_zputfield:__ andl(rax, 0x1); // boolean is true if LSB is 1// fall through to bputfieldcase Bytecodes::_fast_bputfield:__ movb(field, rax);break;case Bytecodes::_fast_sputfield:// fall throughcase Bytecodes::_fast_cputfield:__ movw(field, rax);break;case Bytecodes::_fast_fputfield:__ movflt(field, xmm0);break;case Bytecodes::_fast_dputfield:__ movdbl(field, xmm0);break;default:ShouldNotReachHere();}//判断是否volatile变量__ testl(rdx, rdx);__ jcc(Assembler::zero, notVolatile);volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad |Assembler::StoreStore));__ bind(notVolatile);}
如果是volatile变量,在属性修改完成后就会执行lock addl $0×0,(%rsp);。
lock指令有两个含义:
04
解释器下实现
hotspot解释器模块(hotspot\src\share\vm\interpreter)有两个实现:
C++解释器 : bytecodeInterpreter + cppInterpreter
模板解释器 : templateTable + templateInterpreter
模板解释器我们看完了,下面我们来看一下C++ 解释器的实现。代码位于hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
可以看到,在执行完操作之后,bytecodeInterpreter 增加了 OrderAccess::storeload()`屏障,代码如下:
注意:在java9以后的版本当中,除了之外,都换成了 fence 方法以外,都换成了编译器屏障,如下:
3
总结
你以为就完了,那就太容易,只有理论没有时间自然不能信服,下面我需要用到反汇编指令查看上述的问题。首先,我们用到的linux版本如下
root@ubuntu:~/workplace/javalean/debug/javaclass$ uname -aLinux ubuntu 5.4.0-73-generic #82~18.04.1-Ubuntu SMP Fri Apr 16 15:10:02 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
使用hsdis-amd64.so 首先你需要在网上找一个 hsdis-amd64.so 库,然后放到jdk的目录下/jre/lib/amd66
编写我们的程序,使得程序可以执行 如下:
编译,并加上打印参数如下
- -XX:+UnlockDiagnosticVMOptions
- -XX:PrintAssembly 打印JIT编译后的汇编
- -XX:CompileCommand=print,*MyClass.myMethod 过滤输出
过滤 add 和add2的方法,这里加上了 -Xcomp 确保是使用模板解释器来进行编译
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=compileonly,*VolatileCode::add* VolatileCode
得到结果如下,add 方法下:
add2的方法:
对于add 和add2 来说 add 比add多了一个 lock指令。
