JVM进阶教程之字段访问优化浅析

前言

在实际中,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
  4. 代码可以手工优化成如下
static int bar(Foo o, int x) {
 o.a = x;
 return x;
}

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

字段读取优化

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

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

  • 当即时编译器 遇到对同一字段的读取节点 时,如果缓存值还没有失效,那么将读取节点 替换 为该缓存值
  • 当即时编译器 遇到对同一字段的存储节点 时,会 更新 所缓存的值
    • 当即时编译器遇到 可能更新 字段的节点时,它会采取 保守 的策略, 舍弃所有的缓存值
    • 方法调用节点 :在即时编译器看来,方法调用会执行 未知代码
    • 内存屏障节点 :其他线程可能异步更新了字段

样例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;
}

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • JVM中的守护线程示例详解

    前言 在Java中有两类线程:User Thread(用户线程).Daemon Thread(守护线程) 用个比较通俗的比如,任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作:只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作. Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者. 在之前的<详解JVM如何处理异常>提到了守护线程,

  • JVM处理未捕获异常的方法详解

    前言 继之前的文章详解JVM如何处理异常,今天再次发布一篇比较关联的文章,如题目可知,今天聊一聊在JVM中线程遇到未捕获异常的问题,其中涉及到线程如何处理未捕获异常和一些内容介绍. 什么是未捕获异常 未捕获异常指的是我们在方法体中没有使用try-catch捕获的异常,比如下面的例子 private static void testUncaughtException(String arg) { try { System.out.println(1 / arg.length()); } catch

  • JVM:晚期(运行期)优化的深入理解

    晚期(运行期)优化 在部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码.为了提高热点代码的执行效率,在运行时,虚拟机会将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器 本章提到的编译器.即时编译器都是指HotSpot虚拟机内的即时编译器,虚拟机也是特质HotSpot虚拟机 1.HotSpot虚拟机内的即时编译器 1).解释器与编译器 当程序需要迅速启动和执行的

  • JVM指令的使用深入详解

    一.未归类系列A 此系列暂未归类. 指令码    助记符                            说明 0x00         nop                                什么都不做 0x01        aconst_null                   将null推送至栈顶 二.const系列 该系列命令主要负责把简单的数值类型送到栈顶.该系列命令不带参数.注意只把简单的数值类型送到栈顶时,才使用如下的命令. 比如对应int型才该方式只能把

  • 详解Java虚拟机(JVM)运行时

    JVM(Java虚拟机)是一个抽象的计算模型.就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域.目的是为构建在其上运行的应用程序提供一个运行环境.JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构.本文主要介绍Java虚拟机(JVM)运行时详解. 我们知道的JVM内存区域有:堆和栈,这是一种泛的分法,也是按运行时区域的一种分法,堆是所有线程共享的一块区域,而栈是线程隔离的,每个线程互不共享. 线程不共享区域 每个线程的数据区域包括

  • 详解JVM 运行时内存使用情况监控

    java 语言, 开发者不能直接控制程序运行内存, 对象的创建都是由类加载器一步步解析, 执行与生成与内存区域中的; 并且jvm有自己的垃圾回收器对内存区域管理, 回收; 但是我们已经可以通过一些工具来在程序运行时查看对应的jvm内存使用情况, 帮助更好的分析与优化我们的代码; 注: 查看系统里java进程信息 // 查看当前机器上所有运行的java进程名称与pid(进程编号) jps -l // 显示指定的jvm进程所有的属性设置和配置参数 jinfo pid 1 . jmap : 内存占用情

  • JVM如何处理异常深入详解

    前言 无论你是使用何种编程语言,在日常的开发过程中,都会不可避免的要处理异常.今天本文将尝试讲解一些JVM如何处理异常问题,希望能够讲清楚这个内部的机制,如果对大家有所启发和帮助,则甚好. 当异常不仅仅是异常 我们在标题中提到了异常,然而这里指的异常并不是单纯的Exception,而是更为宽泛的Throwable.只是我们工作中习以为常的将它们(错误地)这样称谓. 关于Exception和Throwable的关系简单描述一下 Exception属于Throwable的子类,Throwable的另

  • 浅析JVM逃逸的原理及分析

    我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针.当对象不再使用后,需要依靠GC来遍历引用树并回收内存.如果堆中对象数量太多,回收对象还有整理内存,都会会带来时间上的消耗,GC表示压力很大,然后影响性能.所以,在我们日常开发中,内存,时间都是相当的宝贵,该如何优化堆栈开销,是一个比较重要的问题. 在这里,我以逃逸分析角度聊聊JVM优化的那些事儿. 为什么"逃逸" 在计算机语言编译器优化原理中,逃逸分析是指分析指针动态范围的方法,它同编译器优化原理的指针分析和

  • java JVM原理与常识知识点

    JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行. 1.

  • JVM:早期(编译期)优化的深入理解

    早期(编译期)优化 JVM的编译器可以分为三个编译器: 前端编译器:把*.java转变为*.class的过程.如Sun的Javac.Eclipse JDT中的增量式编译器(ECJ) JIT编译器:把字节码转变为机器码的过程,如HotSpot VM的C1.C2编译器 AOT编译器:静态提前编译器,直接将*.java文件编译本地机器代码的过程 本章的后续文字里,"编译期"和"编译器"都仅限于第一类编译过程 1.Javac编译器 Javac编译器本身就是一个由Java语言

随机推荐