【Java虚拟机】第三章、jvm运行期优化,解释器,编译器(AOT静态编译,JIT动态编译)

    已经第三章了,看了前两章是不是有点懵?或者开始意识到了什么?或者整个串联起来了?回顾一下

第一张主要讲的是jvm怎么创建

第二章讲的是jvm内存结构

和番外篇class加载过程

    那么我们再结合这一章解释器和编译器,静态和动态编译,把他们串到1起,简单的总结下jvm被创建后是如何运行的。之后我们要开始学习GC优化了。

    都知道写的好的C/C++运行效率很高,殊不知JAVA也在这方面做努力,下面主要讲解的是java是如何编译和解释上做的努力。首先了解下解释器和编译器。

解释器

    java的语言怎么在windows,os平台上识别呢?就用了解释器从中间转换,要不然机器肯定不知道你要干啥,我讲中文,你只能听懂阿拉伯语,这时候就需要解释器了。但是这种方式有几个老哥举了几个很直白的例子:

    牛吃草,一个草原上,三种牛吃不同的用碾碎机碾碎的三种草,解释器就是要根据不同的牛,碾碎不同的草,给特定的牛吃。每天都要先知道都有什么牛,我要什么草,什么草对应什么牛,这种方法很麻烦。

    牛:各种cpu

    草:java,php等语言

   碾碎机:解释器

    不知道我讲的清晰吗?以上讲的是语言,解释器,机器的关系。官方的定义:直接执行用编程语言编写的指令程序,容易让用户实现程序的跨平台,如java,php等。同一套代码可以在多个机器上运行,而无需根据操作系统进行更改。

    从官方的定义来看,一套代码就可以在多个机器上运行,表面看上去很强大,但其实开销很大,因为每次一个程序执行一次就要被解释一次。

    上面讲到了解释器的作用,讲的比较通俗,再讲一下具体为什么需要解释器吧。因为基于堆栈的本地平台很少,所以大多数本地平台不能直接执行java字节码。为了解决这个问题,早期的jre通过解释字节码来运行JAVA程序。即JVM在一个循环中重复操作:

  1. 获取待执行的下一个字节码
  2. 解码
  3. 从操作数栈获取所需的操作数
  4. 按照JVM规范执行操作
  5. 将结果写回堆栈

这种方法的优点是其简单性:JRE开发人员只要编写代码来处理每种字节码就可以了,并且因为用于描述操作的字节码于255个,所以实现的成本较低。同样比较遗憾的是性能也较低。

要解决与C/C++之间的性能差距意味着,使用不会牺牲可移植性的方式开发用于java平台的本地代码编译

编译器

和解释器不同的是,编译器先把代码编译成机器码,然后每次一段程序运行时,都只运行机器码部分。过程为:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器(Linker) → 可执行程序 (executables)。

参考:https://blog.csdn.net/sunxianghuang/article/details/52094859

1、动态编译(dynamic compilation)指的是“在运行时进行编译”;与之相对的是事前编译(ahead-of-time compilation,简称AOT),也叫静态编译(static compilation)。

2、JIT编译(just-in-time compilation)狭义来说是当某段代码即将第一次被执行时进行编译,因而叫“即时编译”。JIT编译是动态编译的一种特例。JIT编译一词后来被泛化,时常与动态编译等价;但要注意广义与狭义的JIT编译所指的区别。

3、自适应动态编译(adaptive dynamic compilation)也是一种动态编译,但它通常执行的时机比JIT编译迟,先让程序“以某种式”先运行起来,收集一些信息之后再做动态编译。这样的编译可以更加优化。

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

即时编译器并不是虚拟机必要的,但是即时编译器的好坏,优化的程度高低决定了jvm的性能优秀与否的关键之一。它也是虚拟机最核心也是最能体现虚拟机技术水平的部分。

一般情况下解释器和jit编译器是并存的,那么这种方式存在的意义是什么呢?

尽管不是所有的虚拟机都是这种架构,但是许多主流的商用机都是这样实现的。解释器和编译器两者各有优势,当程序需要尽快启动和执行的时候,这时候解释器可以首先发挥作用,省去编译时间,随着程序运行时间越长,当代码中有部分的代码需要不断执行的时候,这时候编译器就发挥了作用,他会把更多的代码编译成本地代码,之后可以获得更高的执行效率。当程序运行环境中内存区域分配限制较大,可以考虑更多的使用解释执行节约内存,反之则使用编译执行提升效率。此外,如果编译后出现“罕见陷阱”,可以通过逆优化退回到解释执行。

解释器与编译器的交互

编译的时间开销

解释器的执行,抽象看是这样的:

输入的代码=>[解释器 解释执行]=>执行结果

而要JIT编译然后再执行的抽象过程是这样的:

输入的代码=>[编译器 编译]=>编译后的代码=>[执行]=>执行结果

说JIT执行的比解释快,一般指的是编译后的运行比解释器运行的块,不包含编译的过程。

JIT编译的再快,至少比解释器执行一次要慢,而要得到最后的结果还需要执行编译后的代码,这一步骤。

所以对只执行一次的代码而言,解释执行总比编译执行要快,那么哪些属于只执行一次的代码呢?下面两个条件同时满足是,那就属于只执行一次的代码:

  1. 只被调用一次的代码,例如类的构造器(class initializer,<client>())
  2. 没有循环

对只执行一次的代码进行编译运行是得不偿失的。对只执行少量次数的代码,JIT编译带来的执行速度的提升也未必能抵消掉最初编译带来的开销。

只有对频繁执行的代码,JIT编译才能保证有正面的收益。

编译的空间开销

对一般的java方法而言,编译后代码的大小相比字节码大小,膨胀比大了10x是很正常的。同上面说的时间开销一样,这里的空间开销也是一样,只有对频繁执行的代码才有编译的意义。如果把所有的代码都进行编译运行,会显著的增大代码所占空间,导致“代码爆炸”。

 

这也就解释了为什么有些JVM会选择不总是做JIT编译,而是选择用解释器+JIT编译器的混合执行引擎。

为何HotSpot虚拟机会实现两种不同方式的即时编译器?

HotSpot中有两种即时编译器:client compile,server compile,简称C1和C2编译器,分别用在客户端和服务端。目前主流的虚拟机采用其中一种编译器和解释器一起工作。程序用哪种编译器取决于虚拟机运行的模式。HotSpot虚拟机会根据自身版本和宿主机器的硬件性能自动选择运行模式。用户也可以使用“-client”或“-server”参数去强制虚拟机使用何种编译器运行。

用client compile的优势为编译效率高C1主要关注点在于局部优化,而放弃许多耗时较长的全局优化手段。

用server compile的优势为编译质量高。C2则是专门面向服务器端的,并为服务端的性能配置特别调整过的编译器,是一个充分优化过的高级编译器。

哪些代码会被编译为本地代码?怎么编译为本地代码?

程序中的代码只要是“热点代码”时,就会编译为本地代码,那么什么叫热点代码?

  1. 被多次调用的方法
  2. 被多次执行的循环体

两种情况,编译器都是以整个方法作为编译对象。这种编译方法因为发生在方法执行过程中,所以形象的称为“栈上替换”(On Stack Replacement,OSR),即方法栈帧还在栈上,方法就被替换了。

热点代码的定义已经清楚了,那么类似于被多次调用这个行为是怎么判定的呢?这种判定的行为被称为“热点探测”。目前主要的热点探测方式有如下两种:

基于采样的热点探测

这个其实很好理解,采用这种方法的虚拟机会周期性的访问栈顶,当一个方法多次出现在栈顶时,会被虚拟机认为这个方法是“热点方法”。这个方法的好处就是简单高效,还很容易的获取方法调用关系(将调用堆栈展开),缺点是很难精确的确认一个方法的热度,容易因为线程阻塞等原因而扰乱热点探测。

基于计数器的热点探测

这个也很简单,就是虚拟机会为每个方法或者代码块建立计数器,统计方法的执行次数,如果执行次数超过一定阀值就认为他是热点方法。这种统计方式稍微比采样的方式更加复杂,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系,但是它的统计结果相对更加精确严谨。

HotSpot是采用的哪种热点探测?

使用的是第二种计数器的热点探测,它为每个方法提供了两种不同的计数器:方法调用计数器和回边计数器。在确定的虚拟机运行参数的情况下,这两个计数器都有一个阀值,超过这这两个计数器的阀值之和就是热点方法,就会触发JIT编译。

方法调用计数器

上面已经说过了这个计数器怎么回事。这个再根据这个计数器描述下,整个运行方式是如何的。首先,当一个方法被调用时,会检查这个方法是否存在被JIT编译过的版本。如果存在,则直接执行编译后的本地代码。如果不存在这个已被编译的版本,那么会在这个方法的调用计数器上+1,然后判断方法调用计数器和回边计数器的总和是否超过方法调用计数器的阀值,如果超过阀值,那么将会向即时编译器发送一个此方法需要被编译的请求。

如果不对编译请求做同步设置,那么执行引擎不会同步的等待编译请求完成,而是继续用解释器执行字节码,直到提交的编译请求完成,并且该方法的入口地址已经指向被编译的方法后(系统自动替换),下一次调用该方法就会执行被编译后的版本。

回边计数器

它的作用是统计一个方法中循环体代码的执行次数,在字节码中遇到控制流向后跳转的指令叫“回边”。

我们了解了解释器和JIT编译器,下面来说下JIT编译器和AOT编译器的区别吧。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值