【一起学习JVM】运行期编译优化

编译期分别有javac编译、JIT编译,上一篇文章介绍了javac编译的过程和优化,这一篇文件介绍JIT编译过程和优化。java程序可以通过解释器直接执行,也可以通过编译器将字节码编译为机器码之后再执行,前者无需编译可直接执行,但是执行的效率并不高,而编译之后的机器码执行效率高,但是编译的过程需要耗费时间。所以当JVM发现某个方法或者代码块的运行频繁,那么就会对热点代码进行编译,进行优化,在执行这个任务的编译器称为即时编译器,简称JIT编译器解释器和编译器JVM中一般都会同时包含解释器和编译器,并且解
摘要由CSDN通过智能技术生成

编译期分别有javac编译、JIT编译,上一篇文章介绍了javac编译的过程和优化,这一篇文件介绍JIT编译过程和优化。
java程序可以通过解释器直接执行,也可以通过编译器将字节码编译为机器码之后再执行,前者无需编译可直接执行,但是执行的效率并不高,而编译之后的机器码执行效率高,但是编译的过程需要耗费时间。所以当JVM发现某个方法或者代码块的运行频繁,那么就会对热点代码进行编译,进行优化,在执行这个任务的编译器称为即时编译器,简称JIT编译器

解释器和编译器

JVM中一般都会同时包含解释器和编译器,并且解释器和编译器各有优势,当程序需要快速启动和执行的时候,解释器无需进行编译可以直接执行。但是当执行的效率需要进一步提高的时候,编译器可以实现将字节码编译为机器码并进行更好的优化,提高代码的执行效率。

编译对象与触发即时编译条件

在运行过程中,对于“热点代码”会触发即时编译,那么什么样的代码会被称为“热点代码”?

编译对象

“热点代码“主要有两类:

  • 被多次调用的方法
    当一个方法被多次调用,该方法可以被称为热点代码,会将整个方法进行编译,提高执行速度。
  • 被多次执行的循环体
    主要是针对当一个方法调用的次数比较少,但是方法内部的循环体执行很多次,那么也有必要对该循环体进行优化,编译器依然会对这个方法进行编译,因为这种方式发生在方法运行的过程中,所以被称为栈上替换(OSR编译)

被多次执行的方法、多次执行的循环体,那么这个多次是多少次呢?

编译条件

判断方法或者循环体是不是被多次执行,是不是热点代码主是通过热点探测判定,判定的方式有两种:

  • 基于采样的热点探测
    JVM会周期性的对每个线程的栈顶进行统计,JVM会维护一个统计表,当多次发现某个方法经常出现在栈顶,那么就认为该方法是热点代码,会进行编译
    优点:实现简单、高效
    缺点:统计不准确,如果线程出现阻塞或者其他的因素影响,那么统计的结果不准确
  • 基于计数器的热点探测
    JVM会给每个方法建立计数器,统计方法的调用次数,当一个方法被调用一次,计数器就会自增,当计数器的次数达到阈值,那么就会触发即时编译。
    HotSpot虚拟机使用的是计数器方式,因此它会给每个方法创建两类计数器:方法调用计数器(统计方法调用次数)、回边计数器(统计循环体的执行次数)

计数器的阈值:在Client模式下为1500次,在Server模式下是10000次,可以通过参数-XX:CompileThreshold指定计数器的触发次数

即使编译过程
  • 方法调用计数器触发编译过程
    当方法被调用时,会查看当前是否有编译的版本,如果有则使用编译版本执行,如果没有,那么计数器会自增1,并判断当前的方法调用次数+回边计数器之和是否达到阈值,如果达到阈值,会向即时编译器提交即使编译请求,如果没有达到阈值,那么默认会使用解释器先进行执行(可以设置为不适用解释器执行,等待编译器编译完成),当编译完成之后,该方法的入口会被替换为编译之后的入口,下次调用方法时就会使用已编译的版本执行

方法调用计数器触发编译过程

  • 回边计数器触发编译过程
    当解释器执行到一条回边指令,会查看当前方法是否有编译好的版本,如果有,则直接使用编译版本执行,如果没有,那么会将回边计数器自增1,然后判断方法调用计数器+回边计数器之和是否大于阈值,如果没有大于阈值,则继续使用解释器执行,如果的大于阈值,则向编译器提交OSR编译请求,并将回边计数器的值降低一些(因为循环执行块,防止频繁触发提交OSR编译请求),然后继续使用解释器执行,等待编译器的编译结果。

回边计数器触发编译过程

编译优化技术

编译的代码执行更快,第一个原因是虚拟机执行字节码会额外消耗时间,第二个原因就是即时编译器在执行编译的时候会对代码进行优化。

公共子表达式消除

如果一个表达式在之前已经计算过了,并且表达式中的变量都没有改变,那么该表达式就是公共子表达式,当再次执行该表达式的时候,并进行重复的计算,而是使用之前计算的值,减少重复计算提高效率。

数组范围检查消除

在访问数组的时候,为了保证访问的数组元素有效,并且可以正常访问,每次通过索引访问数组元素,都隐含了条件判定操作,判断当前访问的索引是否超过数组的长度,是否为数组中的有效索引。如果是循环访问数组中的元素,那么会影响数组的操作效率,所以在运行编译期做的有优化为:确定数组的长度,并且判定代码中访问数组元素的索引是否符合i>0 && i < arr.length的条件,如果符合,那么在运行时就无需判断当前数组的上下界,节省多次条件判断操作,提高执行效率。

方法内联

方法内联是虚拟机最重要的优化手段之一,第一可以消除方法调用的成本(减少栈桢创建),第二为其他优化建立良好的基础

方法内联简单来讲就是将被调用的方法复制到调用方法体中,如果是非虚方法(不是多态)可以直接进行内联,如果是虚方法,那么就需要通过CHA(类型继承关系分析)进行内联

  • 虚方法的内联
    虚方法就是(多态调用的方法),当进行内联的时候,会查询目标下是否有多个方法选择,如果只有一个方法,那么也可以进行内联,内联之后,如果在后续的执行过程中,这个方法的继承关系没有发生改变,那么就可以一直使用,如果这个方法的继承关系发生改变,那么就不能继续使用已经编译的代码,退回到解释器执行或者重新编译。
    如果目标下有多个方法选择,那么编译器会通过内联缓存来完成方法内联,内联缓存会记录目标方法的信息,然后进行内联,每次目标方法被调用时都会比较目标方法是否和当前记录方法一致,如果一致则可以一致使用方法内联,如果不一致,那么就会取消内联。
逃逸分析

逃逸分析的基本行为是分析对象的动态作用域: 当一个对象在方法中被定义之后,如果该对象没有被堆中的对象关联,没有传递到未知方法、没有座位方法的返回值进行返回,那么该对象没有发生方法逃逸,否则称为方法逃逸。如果一个变量可能被其他的线程访问到,则称为线程逃逸
如果一个对象没有发生逃逸,那么可以对该变量进行高效的优化:

  • 栈上分配
    当一个对象不会逃逸到方法外的时候,该对象可以在栈中分配内存,对象所占用的内存空间随着栈桢的出栈自动销毁,减小堆中的GC的次数和STW时间
  • 同步消除
    如果逃逸分析可以确定一个变量不会发生逃逸,即不会发生线程逃逸,那么该变量的啊哦做会被移除到同步代码块之外,因为同步操作是一个相对耗时的过程
  • 标量替换
    标量:一个数据无法再分解为更小的数据表示,称为标量,如果一个对象不会发生方法逃逸,并且这个对象可以被拆解的话,那么就会使用这个对象中的成员变量分离出来表示该对象,这样可能会使用到高速寄存器,并且创建对象时额外的对象头、对齐填充等空间也无需创建,节省内存空间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值