字段访问
编绎器对于读取及存储指令的优化
对于读取,如果涉及一个堆对象同一个字段的多次读取,且不涉及该字段的写操作则会优化成方法执行中的缓存。只在第一次读取该对象字段时会从堆中取。会将该对象字段值缓存在局部变量表中。后面读取从这个地方取。这就涉及到一个多线程的可见性问题。如果想要去除可以设置为volatile或加lock,syncronized锁。
对于写操作,如果一个方法体对于一个变量的写是会覆盖的,则只会保留最后一个写操作。
还有一个优化则可以对于不可到达的分支代码进行消除,本质上都是减少不执行的代码,减少编绎成机器码后存储到code cache中的大小。
原来:工作内存与主内存的模型本质上是 缓存字段的值到局部变量表,所谓加锁,volatile字段是不读取缓存的字段。
字段读取优化
即时编译器会优化实例字段以及静态字段访问,以减少总的内存访问数目。具体来说,它将沿着控制流,缓存各个字段存储节点将要存储的值,或者字段读取节点所得到的值。当即时编译器遇到对同一字段的读取节点时,如果缓存值还没有失效,那么它会将读取节点替换为该缓存值。当即时编译器遇到对同一字段的存储节点时,它会更新所缓存的值。当即时编译器遇到可能更新字段的节点时,如方法调用节点(在即时编译器看来,方法调用会执行未知代码),或者内存屏障节点(其他线程可能异步更新了字段),那么它会采取保守的策略,舍弃所有缓存值。
Java 内存模型时规定,可以通过 volatile 关键字标记实例字段a,以此强制对它的读取。
实际上,即时编译器将在 volatile 字段访问前后插入内存屏障节点。这些内存屏障节点会阻止即时编译器将屏障之前所缓存的值用于屏障之后的读取节点之上。
就我们的例子而言:
class Foo {
volatile a;
void bar() {
a = true;
while (a) {}
}
void whatever() { a = false; }
}
在 X86_64 平台上,volatile 字段读取操作前后的内存屏障是 no-op,在即时编译过程中的屏障节点,还是会阻止即时编译器的字段读取优化,强制在循环中使用内存读取指令访问实例字段Foo.a的最新值。同理,加锁、解锁操作也同样会阻止即时编译器的字段读取优化。
示例总结
class Bar {
int a = 0;
void set() {
a = 10;
}
int get(){
return a;
}
}
线程A调用set方法,读取a,缓存a字段到set方法的局部变量区。然后修改a=10,更新缓存,写入堆主内存。
线程B调用get方法,读取a,缓存a字段到get方法的局部变量区。
由于线程A的更新缓存,写入主内存不是原子的,因此B线程可能先读取到a字段。因此导致的并发问题。
解决方案为:加锁,或增加volatile修饰,JVM通过内存模型(内存屏障)机制,B直接读取主内存字段,而不读取缓存的字段。如果A线程先写入a字段,则主内存是更新后的值,B访问后得到最新的值。