侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

JVM-解释器和编译器

再见理想
2022-05-25 / 0 评论 / 0 点赞 / 405 阅读 / 3,136 字

一,解释器与编译器

1.1 解析器

在运行时,通过解释器逐行解释执行程序,其角色就是一个翻译者,将字节码的内容翻译为对应平台的本地机器指令。java.exe 可以简单看成是 Java 解释器。

可以分为 字节码解释器 和 模版解释器:

1.1.1 字节码解释器

字节码解释器就是在执行时通过纯软件代码模拟字节码的执行,效率非常低下。

1.1.2 模版解释器

模板解释器将每一条字节码和模版函数相关联,直接产生这个字节码执行时的机器码,提高性能。


1.1.3 解释器组成

在 hotspot 虚拟机中,解释器主要由 Interpreter 模块 和 Code 模块 组成。

Interpreter 模块:实现了解释器核心功能;

Code 模块:用于管理Hotspot虚拟机运行时生成的本地机器指令。


1.2 编译过程中三类编译器

周志明对编译器的"分类"的看法,编译过程中有三类比较有有代表性的编译器:

前端编译器:Sun的 javac,Eclipse 的增量编译器 EJC。把*.java编译为*.class

JIT编译器:Hotsport VM 的c1,c2编译器把字节码编译为机器码

AOT编译器:GCJ(GNU Compiler for Java )、excelsior JET。*.java 文件编译成本地机器代码


1.2.1 前端编译器

Javac 做了许多针对 Java 语言编码过程的优化措施来改善程序员的编码风格和提高编码效率。相当多新生的 Java 语法特性,都是靠编译器的 语法糖 来实现,而不是依赖虚拟机的底层改进来支持,可以说,Java 中即时编译器在运行期的优化过程对于程序运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更加密切。


1.2.2 编译器优化重点

虚拟机设计团队把对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由 Javac 产生的 Class 文件(如 JRuby、Groovy 等语言的 Class 文件)也同样能享受到编译器优化所带来的好处


1.3 解释器与编译器并存

为何HotSpot虚拟机要使用解释器与编译器并存的架构?

当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行,编译器不同,它要发挥作用,把代码编译成本地代码需要一定的执行时间。但是编译为本地代码之后,执行效率很高。(例如JRockit VM尽管执行性能非常高效,但程序启动时必然花费更长的时间来进行编译,对于服务端来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要解释器和即使编译器并存的架构来换取一个平衡点了)

尽管并不是所有的 Java 虚拟机都采用解释器与编译器并存的架构,但许多主流的商用虚拟机,如 HotSpot、J9 等,都同时包含解释器与编译器。解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。

解析器和编译器一般都配合使用:

二,即时编译器

在部分的商用虚拟机(Sun HotSpot、IBM J9)中,Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为热点代码为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为 **即时编译器**(Just InTime Compiler,下文中简称JIT编译器)。

即时编译器并不是虚拟机必需的部分,Java虚拟机规范并没有规定Java虚拟机内必须要有即时编译器存在。但是,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一。


2.1 虚拟机中的即时编译器

HotSpot虚拟机中内置了两个即时编译器,分别称为 Client CompilerServer Compiler,或者简称为C1编译器和C2编译器。目前主流的HotSpot 虚拟机(Sun系列JDK 1.7及之前版本的虚拟机)中,默认采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,HotSpot 虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用 -client-server 参数去强制指定虚拟机运行在 Client 模式或 Server 模式。


2.2 虚拟机中混合模式、解释模式和编译模式

无论采用的编译器是 Client Compiler 还是 Server Compiler,解释器与编译器搭配使用的方式在虚拟机中称为 混合模式(Mixed Mode),用户可以使用参数 -Xint 强制虚拟机运行于 解释模式(Interpreted Mode),这时编译器完全不介入工作,全部代码都使用解释方式执行。另外,也可以使用参数 -Xcomp 强制虚拟机运行于 编译模式(Compiled Mode),这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。

可以通过虚拟机的 java -version 命令的输出结果显示出这3种模式:


2.3 热点代码触发条件

在运行过程中会被即时编译器编译的“热点代码”有两类,即:

被多次调用的方法;

被多次执行的循环体。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot SpotDetection),目前主要的热点探测判定方式有两种,分别如下:

1,基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是 热点方法。

2,基于计数器的热点探测(HotSpot 默认):采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法。


2.4 两类计数器

在 HotSpot 虚拟机中使用的是第二种——基于计数器的热点探测方法,因此它为每个方法准备了两类计数器:方法调用计数器 和 回边计数器。

在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。


2.4.1 方法调用计数器

顾名思义,这个计数器就用于统计方法被调用的次数,它的默认阈值在 Client 模式下是1500次,在 Server 模式下是10 000次,这个阈值可以通过虚拟机参数 -XX:CompileThreshold 来人为设定。

整个JIT编译的交互过程如图:


2.4.2 回边计数器

它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为回边(Back Edge)。显然,建立回边计数器统计的目的就是为了触发 OSR 编译(栈上替换)。

当解释器遇到一条回边指令时,会先查找将要执行的代码片段是否有已经编译好的版本,如果有,它将会优先执行已编译的代码,否则就把回边计数器的值加1,然后判断方法调用计数器与回边计数器值之和是否超过回边计数器的阈值。当超过阈值的时候,将会提交一个 OSR 编译请求(栈上替换),并且把回边计数器的值降低一些,以便继续在解释器中执行循环,等待编译器输出编译结果,整个执行过程如图:


2.4.3 标准编译和栈上替换

当 JVM 执行一个 Java 方法,它会检查方法调用计数器和回边计数器的总和,以决定这个方法是否有资格被编译。如果有,则这个方法将排队等待编译。这种编译形式并没有一个官方的名字,但是一般被叫做标准编译

这种编译是一个异步的过程,它允许程序在代码正在编译时被继续执行。

但是如果方法里有一个很长的循环或者是一个永远都不会退出并提供了所有逻辑的程序会怎么样呢?这种情况下,JVM 需要编译循环而并不等待方法被调用。所以每执行完一次循环,分支计数器都会自增和自检。如果分支计数器计数超出其自身阈值,那么这个循环(并不是整个方法)将具有被编译资格。

这种编译叫做栈上替换(OSR),JVM 必须有能力当循环正在运行时,开始执行此循环已被编译的版本。程序执行过程中,动态替换掉 Java 方法栈帧,从而使得程序能够在非方法入口处进行解释执行和编译后的代码之间的切换。换句话说,如果一个循环被栈上替换方式所编译,那么下一次循环迭代则会执行新编译的代码。 默认情况C1的 OSR 编译的阈值为 13500,C2 为 10700。


2.5 编译流程

当一个方法被调用时,会先检查该方法是否存在JIT编译过版本,如果存在,则优先使用编译后的本地代码来执行,如果不存在已经被编译过的版本,则将此方法的调用计数器值+1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值,如果已超过阈值,那么将会向即使编译器提交一个该方法的代码编译请求,流程图:


2.6 编译优化

JIT除了具有缓存的功能外,还会对代码做各种优化,包括:逃逸分析、 锁消除、 锁膨胀、 方法内联、 空值检查消除、 类型检测消除、 公共子表达式消除。

相关文章

简书-即时编译(JIT)
博客园-jvm 解释器和JIT编译器

0

评论区