在实际中,Java程序中的对象或许 本身就是逃逸 的,或许因为 方法内联不够彻底 而被即时编译器 当成是逃逸 的,这两种情况都将
导致即时编译器 无法进行标量替换 ,这时,针对对象字段访问的优化显得更为重要。
static int bar(Foo o, int x) { o.a = x; return o.a; }
static int bar(Foo o, int x) { o.a = x; return x; }
即时编译器也能作出类似的 自动优化
static int bar(Foo o, int x) { int y = o.a + x; return o.a + y; }
实例字段Foo.a被读取两次,即时编译器会将第一次读取的值缓存起来,并且 替换 第二次的字段读取操作,以 节省 一次内存访问
static int bar(Foo o, int x) { int t = o.a; int y = t + x; return t + y; }
static int bar(Foo o, int x) { o.a = 1; if (o.a >= 0) return x; else return -x; }
字段读取节点被替换成一个 常量 ,进一步触发更多的优化
static int bar(Foo o, int x) { o.a = 1; return x; }
class Foo { boolean a; void bar() { a = true; while (a) {} } void whatever() { a = false; } }
即时编译器会将while循环中读取实例字段a的操作 直接替换为常量true
void bar() { a = true; while (true) {} } // 生成的机器码将陷入这一死循环中 0x066b: mov r11,QWORD PTR [r15+0x70] // 安全点测试 0x066f: test DWORD PTR [r11],eax // 安全点测试 0x0672: jmp 0x066b // while (true)
如果一个字段先后被存储了两次,而且这 两次存储之间没有对第一次存储内容读取 ,那么即时编译器将 消除 第一个字段存储
class Foo { int a = 0; void bar() { a = 1; a = 2; } }
即时编译器将消除bar方法的冗余存储
void bar() { a = 2; }
即便在某个字段的两个存储操作之间读取该字段,即时编译器也可能在 字段读取优化 的帮助下,将第一个存储操作当作 冗余存储
场景:例如两个存储操作之间隔着许多代码,又或者因为 方法内联 的原因,将两个存储操作纳入到同一编译单元里(如构造器中字段的初始化以及随后的更新)
class Foo { int a = 0; void bar() { a = 1; int t = a; a = t + 2; } } // 优化为 class Foo { int a = 0; void bar() { a = 1; int t = 1; a = t + 2; } } // 进一步优化为 class Foo { int a = 0; void bar() { a = 3; } }
如果所存储的字段被标记为 volatile ,那么即时编译器也 不能消除冗余存储
int bar(int x, int y) { int t = x*y; t = x+y; return t; }
没有节点依赖于t的第一个值 x*y
,因此该乘法运算将被消除
int bar(int x, int y) { return x+y; }
int bar(boolean f, int x, int y) { int t = x*y; if (f) t = x+y; return t; }
部分程序路径上有冗余存储(f=true),该路径上的乘法运算将会被消除
int bar(boolean f, int x, int y) { int t; if (f) t = x+y; else t = x*y; return t; }
int bar(int x) { if (false) return x; else return -x; }
不可达分支指的是任何程序路径都不可达到的分支,即时编译器将 消除不可达分支
int bar(int x) { return -x; }
转载请注明出处:http://zhongmingmao.me/2019/01/06/jvm-advanced-optimization-filed-access/
访问原文「 JVM进阶 -- 浅谈字段访问优化 」获取最佳阅读体验并参与讨论