我看Java虚拟机(7)---解释器和JIT编译器

时间:2021-10-13 14:00:25

Java是被定为为解释性语言,JIT编译器并不是强制需要的,也并非所有的虚拟机都是用解释器+编译器的并存架构。但主流的商用虚拟机如Hotspot、J9等都采用这种并存的架构。

解释器和编译器比较

解释器优点:省去编译时间,启动速度快
编译器优点:对代码进行优化,执行效率高
两种方式的优点各为对方的缺点。即解释器的缺点是执行效率低下,编译器的缺点是启动速度慢。很容易理解。

Java虚拟机

由于Java虚拟机的平台无关性,它比C编译器多了一个字节码的步骤,所以会显得性能效率比较鸡肋。所以Java虚拟机采用这种解释器和编译器(两种模式:client(也叫c1)模式和server(c2)模式)并存的方式,对速度和效率做了一个trade off。
简单来讲,Java虚拟机就是将使用频率高的“热点代码”,触发即时编译器将其编译为本地代码,以后每次使用时直接调用本地代码。那么问题来了:

  1. 怎么判定热点代码
  2. 判定出来后怎么处理

热点代码的判定

目前主要有两种方式探测热点代码:
1. 基于采样的热点探测:虚拟机周期性检测各个线程的栈顶方法,若是某个方法经常出现,则判定该方法为热点代码。
优点:实现简单;
缺点:不精确,比如当某个线程处于线程阻塞时,会扰乱热点探测。
2. 基于计数器的热点探测:为每个方法建立计数器,统计执行次数,若超过一定阈(yu)值,则认为它是“热点方法”。
优点:精确;
缺点:实现复杂,要为每个方法维护计数器。

热点代码的处理

基于计数器的探测,它为每个方法准备了两个计数器:方法调用计数器和回边计数器。
这两个计数器都有各自的一个阈值,若超过阈值则触发JIT编译。

  1. 方法调用计数器:用于统计该方法被调用的次数,c1模式下默认5000次,c2模式下10000次。执行过程如下:判断是否已存在编译版本,如已存在,则执行编译版本;否则,方法计数器+1,判断两个计数器之和(注意:是方法计数器和回边计数器的和)是否超过方法计数器阈值,超过则向编译器提交编译请求,然后和不超过阈值情况下的处理方式一样,仍旧解释执行该方法。
    PS:该计数器并不是绝对次数,而是相对的执行次数,即在一段时间内的执行次数,当超过一定的时间限度,若还是没有达到阈值,那么它的计数器会减少一半,此过程被称为热度衰减。
  2. 回边计数器:用于统计方法中循环体代码的执行次数,字节码中遇到控制流向后跳转的指令称为“回边”。建立该计数器的目的就是为触发OSR(On StackReplacement)编译,即栈上替换。
    和方法计数器执行过程不同的是:当两个计数器之和超过阈值的时候,它向编译器提交OSR编译,并且调整回边计数器值,然后仍旧以解释方式执行下去。
    PS:该计数器是绝对次数,没有热度衰减。

其他编译优化技术

  1. 公共子表达式消除int d = (c * b) * 12 +(a + b * c)不优化时,b*c将会被计算两次,优化以后就相当于:
E = c * b;
int d = E * 12 + (a + E);

省去一次计算。
2. 数组边界检查消除
比如:前面代码使用过f[3],并且判断了0<=3<=f.length,则下次使用f[3]时,判断可省略。
3. 方法内联:对于非虚方法就不讲了;对于Java的虚方法,方法内联就会出现问题,到底选择哪一个版本的实现,就是一个问题,Java虚拟机引入了一种名为“类型继承关系分析”(Class Hierarchy Analysis,CHA)的技术。 该方法检测出多个版本的实现时,它使用内联缓存,即,第一次调用发生时,记录该接收者信息,缓存该方法,下次发生调用时,比较接收者信息,相同则使用缓存中的方法,否则取消该内联缓存。
4.逃逸分析:分析对象的动态作用域,当一个对象在方法里被定义后,他可能被外部方法调用,这种行为被称为方法逃逸。甚至还可能被外部线程访问,这种行为被称为线程逃逸。 如果能保证一个对象不发生方法逃逸或线程逃逸,则就能对这个变量进行一些高校的优化,比如:栈上分配,同步消除,标量替换等。