替换对象所有字段_JVM字段访问优化

只有经历过地狱般的磨砺,才能练就创造天堂的力量;只有流过血的手指,才能弹出世间的绝响。——泰戈尔

31b1de5be49ea07c742165367c38af63.png

在实际中,Java程序中的对象或许 本身就是逃逸 的,或许因为 方法内联不够彻底 而被即时编译器 当成是逃逸 的,这两种情况都将导致即时编译器 无法进行标量替换 ,这时,针对对象字段访问的优化显得更为重要。

static int bar(Foo o, int x) {

    o.a = x; return o.a;

}

1.对象o是传入参数, 不属于逃逸分析的范围 (JVM中的逃逸分析针对的是 新建对象 )

2.该方法会将所传入的int型参数x的值存储至实例字段Foo.a中,然后再读取并返回同一字段的值

3.这段代码涉及 两次 内存访问操作:存储和读取实例字段Foo.a

代码可以手工优化成如下

static int bar(Foo o, int x) { 

    o.a = x; 

    return x; 

}

即时编译器也能作出类似的 自动优化

字段读取优化

即时编译器会优化 实例字段 和 静态字段 的访问,以 减少总的内存访问次数

即时编译器将 沿着控制流 ,缓存各个字段 存储节点 将要存储的值,或者字段 读取节点 所得到的值

1.当即时编译器 遇到对同一字段的读取节点 时,如果缓存值还没有失效,那么将读取节点 替换 为该缓存值

2.当即时编译器 遇到对同一字段的存储节点 时,会 更新 所缓存的值

3.当即时编译器遇到 可能更新 字段的节点时,它会采取 保守 的策略, 舍弃所有的缓存值

4.方法调用节点 :在即时编译器看来,方法调用会执行 未知代码

5.内存屏障节点 :其他线程可能异步更新了字段

样例1

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; 

}

样例2

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; 

}

样例3

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)

1、可以通过 volatile 关键字标记实例字段a,以 强制 对a的读取

2、实际上,即时编译器将 在volatile字段访问前后插入内存屏障节点

  • 这些 内存屏障节点 将 阻止 即时编译器 将屏障之前所缓存的值用于屏障之后的读取节点之上

  • 在X86_64平台上,volatile字段读取前后的内存屏障都是no-op

  • 在 即时编译过程中的屏障节点 ,还是会 阻止即时编译器的字段读取优化

  • 强制在循环中使用 内存读取指令 访问实例字段Foo.a的最新值

3、同理, 加解锁操作同样也会阻止即时编译器的字段读取优化

字段存储优化

如果一个字段先后被存储了两次,而且这 两次存储之间没有对第一次存储内容读取 ,那么即时编译器将 消除 第一个字段存储

样例1

class Foo { 

    int a = 0;

    void bar() {

         a = 1;

         a = 2; 

     } 

}

即时编译器将消除bar方法的冗余存储

void bar() { a = 2; }

样例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 ,那么即时编译器也 不能消除冗余存储

死代码消除

样例1

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;

}

样例2

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;

}

样例3

int bar(int x) {

    if (false)  return x;

    else  return -x; 

}

不可达分支指的是任何程序路径都不可达到的分支,即时编译器将 消除不可达分支

int bar(int x) { return -x; }

总结

今天介绍了即时编译器关于字段访问的优化方式,以及死代码消除。

即时编译器将沿着控制流缓存字段存储、读取的值,并在接下来的字段读取操作时直接使用该缓存值。

这要求生成缓存值的访问以及使用缓存值的读取之间没有方法调用、内存屏障,或者其他可能存储该字段的节点。

即时编译器还会优化冗余的字段存储操作。如果一个字段的两次存储之间没有对该字段的读取操作、方法调用以及内存屏障,那么即时编译器可以将第一个冗余的存储操作给消除掉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值