Java虚拟机是怎样运行Java字节码的?

以标准 JDK 中的 HotSpot 虚拟机为例,从虚拟机以及底层硬件两个角度来看Java 虚拟机具体是怎么运行 Java 字节码的。

 

虚拟机视角

从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。

加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。

在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。

这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。 

当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。

 

硬件视角

从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。

在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;

第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。

前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。

 HotSpot 默认采用混合模式,综合了解释执行和即时编译两者的优点。

它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。

 

即时编译

即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;

另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。

 

为了满足不同用户场景的需要,HotSpot 内置了多个即时编译器:C1、C2 和 Graal。

Graal 是 Java 10 正式引入的实验性即时编译器,这里暂不做讨论。

 

之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。

 

C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。

C2 又叫做 Server 编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂。

因此编译时间较长,但同时生成代码的执行效率较高。

从 Java 7 开始,HotSpot 默认采用分层编译的方式:热点方法首先会被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。

为了不干扰应用的正常运行,HotSpot 的即时编译是放在额外的编译线程中进行的。

HotSpot 会根据 CPU 的数量设置编译线程的数目,并且按 1:2 的比例配置给 C1 及 C2 编译器。

在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。

编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章