阿里P9工程师带你三分钟深入解析java虚拟机

指令重排序有且只有一条规则,即指令重排序不会改变单线程程序的语意,除此之外没有任何限制。如果编译器发现将一个写操作放到读操作后面可能会提升性能,同时这样做不会改变单线程程序的语意,那么编译器就会对代码进行重排序,如代码清单6-1所示:代码清单6-1 编译器重排序(C++)int v1, v2;
void foo(){
v1 = v2 + 1;
v2 = 0;
}代码中v1位于v2前面,使用gcc 9.2 -O3编译后可得到如代码清单6-2所示的指令:代码清单6-2 编译器重排序(汇编)foo:
mov eax, DWORD PTR v2[rip]
mov DWORD PTR v2[rip], 0
add eax, 1
mov DWORD PTR v1[rip], eax
ret在编译后的代码中,v2先于v1赋值。如果是多线程程序,开发者认为代码顺序就是执行顺序,即v1先于v2执行,就可能产生错误。对于编译器重排序,可以使用编译器提供的编译器屏障(Compiler Barrier)阻止,如GCC使用代码清单6-3所示的编译器屏障阻止重排序:代码清单6-3 编译器屏障__asm__ volatile ("" : : : “memory”);代码清单6-4演示了如何在v1与v2之间插入编译器屏障解决编译器重排序的问题:代码清单6-4 插入编译器屏障(C++)int v1, v2;
void foo(){
v1 = v2 + 1;
asm volatile ("" : : : “memory”);
v2 = 0;
}再次编译后得到如代码清单6-5所示的汇编代码:代码清单6-5 插入编译器屏障(汇编)foo:
mov eax, DWORD PTR v2[rip]
add eax, 1
mov DWORD PTR v1[rip], eax
mov DWORD PTR v2[rip], 0ret在编译后的代码中,v2先于v1赋值,代码没有被编译器重排序,编译器屏障被证明为有效。处理器重排序编译器屏障解决了编译器重排序问题,但是并不能完全解决问题,即使消除了编译器重排序,CPU也可能对指令进行重排序,出现类似编译器重排序后的代码序列。CPU级的指令重排序又与CPU架构相关,具体如图6-1所示。如果把指令抽象为读和写两类,那么两者组合后共有四种重排序规则。注意,x86只允许一种重排序规则,即Store操作被重排序到Load后面,而原来的StoreLoad操作变成LoadStore操作,对于CPU级别的指令重排序,我们需要同样由CPU指令集提供的内存屏障(MemoryBarrier)指令来阻止。在HotSpot VM中,指令内存屏障的实现位于OrderAccess模块,以x86为例,它的各种内存屏障实现如代码清单6-6所示:代码清单6-6 x86的OrderAccessstatic inline void compiler_barrier() {
asm volatile ("" : : : “memory”);
}
inline void OrderAccess::loadload() { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore() { compiler_barrier(); }
inline void OrderAccess::storeload() { fence(); }
inline void OrderAccess::fence() {
#ifdef AMD64
asm volatile (“lock; addl $0,0(%%rsp)” : : : “cc”, “memory”);
#else
asm volatile (“lock; addl $0,0(%%esp)” : : : “cc”, “memory”);
#endif
compiler_barrier();
}上面的代码是GCC的扩展内联汇编形式,这里的关键字volatile表示禁止编译器优化汇编代码。memory告知编译器汇编代码执行内存读取和写入操作,编译器可能需要在执行汇编前将一些指定的寄存器刷入内存。由于x86只支持StoreLoad重排序,所以x86上的OrderAccess只实现了storeload(),对于其他重排序类型,可以使用编译器屏障简单代替。虽然x86指令集有专门的内存屏障指令,如lfence、sfence、mfence,但是OrderAccess::storeload()使用了指令加上lock前缀来当作内存屏障指令,因为lock指令前缀具有内存屏障的语意且有时候比mfence等指令的开销小。除了LoadLoad、LoadStore、StoreStore、StoreLoad这四种基本内存屏障外,HotSpot VM还定义了特殊的acquire和release内存屏障:acquire防止它后面的读写操作重排序到acquire的前面;release防止它前面的读写操作重排序到release后面。acqure和release两者放在一起就像一个“栅栏”,可禁止“栅栏”内的事务跑到“栅栏”外,但是它不阻止“栅栏”外的事务跑到“栅栏”内部。之所以说acquire和release特殊是因为它们两个可以通过基本内存屏障组合而成:acquire可由LoadLoad和LoadStore组合而成,release可由StoreStore和LoadStore组合而成。另一个值得注意的地方是acquire和release都没有使用StoreLoad屏障,这意味着x86架构原生就具有acquire和release语意。在Java层面操作内存屏障的办法是Unsafe.loadFence()、Unsafe.storeFence()和Unsafe.fullFence(),它们分别对应OrderAccess::acquire()、OrderAccess::release()、OrderAccess::fence()[1]。注意,四种基本内存屏障是无法在Java层直接使用的。如何放置内存屏障是极具挑战的,它们通常出现在高级并发编程中,是专家级并发开发者的任务,在大多数情况下缺少它们不会产生影响,但是在高并发场景下缺少它们通常是致命的。HotSpot VM内部使用了大量的内存屏障,如代码清单6-7所示:代码清单6-7 OrderAccess的使用void Method::set_code(…) {

OrderAccess::storestore();
mh->_from_compiled_entry = code->verified_entry_point();
OrderAccess::storestore();
if (!mh->is_method_handle_intrinsic())
mh->_from_interpreted_entry = mh->get_i2c_entry();
}由于解释器会从_from_interpretered_entry跳转到_from_compiled_entry,所以在_from_interpretered_entry设置好后必须保证_from_compiled_entry可用,如果没有内存屏障,CPU可能会将_from_compiled_entry的设置重排序到_from_interpretered_entry后面导致错误,所以需要OrderAccess::storestore指明禁止弱内存模型的StoreStore指令重排序。借助这些内存屏障,现在我们可以开始定义一个语义良好、可预测的内存模型。
本文给大家讲解的内容是深入解析java虚拟机,想继续看更精彩的可以关注一下;或加下QQ群!864488310

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值