JVM字节码执行引擎和编译、优化

Java作为跨平台的语言离不开JVM的支持,Java语言首先经过Javac编译器编译为class字节码文件然后被JVM解释、编译为本地机器码,然后被os执行,由于JVM中间这一层的存在所以Java语言才能跨平台,同时除了Java,ruby、scala等语言经过编译后的字节码只要符合JVM字节码规范也能够被JVM执行,所以字节码是一种规范,了解JVM执行引擎对学习Java也很有帮助。

字节码执行引擎

运行时栈帧结构

栈帧是支持JVM方法调用、执行的数据结构,是JVM运行时虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态链接、方法返回地址,每一个方法调用到结束对应了一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了方法参数和局部变量,以slot(变量槽)为最小单位,操作数栈是一个后入先出的栈(LIFO),32位,方法返回值用于方法执行完将返回值返回上层方法调用,对于异常抛出未处理时是没有返回值(异常表处理),修改PC计数器。

方法调用

方法调用确定被调用方法的版本(具体来说是调用哪一个方法),class文件的编译不包括链接步骤,方法调用在class文件中是符号引用,需要转化为直接引用(入口地址)。

解析调用

在类加载阶段把符号引用转化为直接引用,在方法运行前有可确定的调用版本并且在运行时不可变,也就是说在编译时确定了方法的调用。Java中方法调用的直接码指令有:
1.invokestatic 静态方法
2.invokespecial 实例构造器、私有方法、父类方法
3.invokeinterface方法接口,运行时确定是实现接口的对象
4.invokedynamic
5.invokevirtual虚方法
其中invoke static、invoke special都可以在解析阶段确定,invoke virtual中带有final的方法。解析调用是静态过程,在编译期确定了,不会到运行期再去完成。

分派

分派调用可能是静态的也可能是动态的。
静态分派:重载,Java中变量有静态类型(外观类型)、实际类型。静态类型在编译期已知,实际类型在运行期才可以确定,所以重载是属于静态分派,在编译期已经确定了,并不是由JVM来确定,重载时也会选择最佳的匹配方法。
动态分派:重写,根据对象实际类型选择方法调用。动态分派过程如下:
(1)找到对象实际类型C
(2)在类型C中找到与常量描述都相同的方法,进行访问权限校验,通过返回直接引用否则返回java.lang.IllegalAccessError
(3)否则,按照继承关系对父类进行第二步的搜索验证
(4)没有找到合适方法,抛出java.lang;AbstractMethodError
其中,静态分派调用是一方面会判断对象的静态类型也会判断方法的参数,所以它属于静态多分派;动态分派只会根据实际类型判断,所以属于动态多分派。
动态分派在实现时,jvm在方法区存放了一个虚方法表,同样invokeinterface存在一个接口方法表,虚方法表中存放了方法的实际入口地址,若子类未复写则子类和父类地址一致,指向父类入口,若复写了则指向子类实现的入口地址,方法表在类加载阶段连接是进行,初始化类变量后对方法表也进行了初始化。

编译、优化

Java语言的编译阶段主要有2种,包括了前端编译期(Java->class)、运行期编译期(JIT编译器,字节码->机器码)。

早期编译

Java编译器本身由Java语言编写,它对代码的运行效率几乎没有任何优化措施,性能优化主要集中在JIT中。Javac编译器的编译过程主要有3个过程:
1.解析和填充符号表过程
词法分析、语法分析;填充符号表
2.插入式注解处理器的注解处理过程
注解支持
3.分析和字节码生产过程
语义分析(标注检查、数据流、控制流分析);解语法糖(泛型、自动装箱);字节码生成

晚期优化

1.混合编译模式
Java程序最初通过解释器进行解释执行,当JVM发现某个代码运行频繁时,将把这些代码认为“热点代码”,在运行时把这些代码编译成本地机器码进行各种层次优化,这些都由JIT编译器完成,下面以Hotspot为例介绍运行期的优化方法。
主流的JVM都同时包含了解释器和编译器。解释器省去编译时间可以迅速启动,在程序运行中编译器可以把代码编译成本地代码提高执行效率,而且当编译器激进优化不成立时能够通过逆优化退回到解释执行。HotSpot中设置了2个即时编译器,分别为client、server,server优化程度更深更复杂。
2.编译优化触发条件
被编译优化的“热点代码”主要有2类:多次调用的方法、多次执行的循环体。第一种编译器以整个方法作为编译对象,后一种编译由循环体触发,但JIT还是会以整个方法(而不是循环体)作为编译对象,这种方式发生在方法执行过程中,成为栈上替换(on stack replacement)OSR,方法在栈上,方法被替换了。

热点代码的判定方法主要有2种:采样热点探测、计数器探测
1.基于采样热点探测:jvm周期性检查线程栈顶,某个方法经常出现则为热点代码,并能发现方法的调用关系,但容易受外界干扰(线程阻塞)
2.基于计数器热点探测:建立计数器,统计方法执行次数,超过阈值即为热点代码,但不能发现方法调用关系
Hotspot以第二种方法判断热点代码,计数器有方法计数器和回边计数器,方法调用计数器、回边计数器运行过程分别如下:
img
img
如果不做设置,方法调用计数器统计的不是绝对次数,而是相对次数,在一定时间的调用次数,超过一定时间会减半即热衰减,也可以设置来关闭热衰减。回边计数器,统计了方法中循环体的次数,回边计数器没有热衰退。在方法调用发出即使编译请求后,在代码编译未完成前仍然按解释方式执行,编译过程在后台编译线程中执行,也可以设置关闭后台编译,等待编译完成在执行编译后本地代码。

编译方式比解释方式执行速度快,一方面是执行本地代码快,另一方面JIT编译时进行了优化,主要的优化有:
1.公共子表达式消除
2.数组范围检查消除
3.方法内联
4.逃逸分析

谢谢大佬的打赏!