Java代码执行流程
- java代码编译成为.class文件后,会生成对应的字节码指令,然后经过JVM识别后调用字节码执行引擎来将字节码指令转换成可供CPU执行的机器码(0101010),在这个过程中,有两个比较重要的组件:解释器,即时编译器
JVM执行引擎
- 执行引擎是Java虚拟机四大组成部分中的一个核心组成(另外三个分别是类加载器子系统,运行数据区,垃圾回收器),java虚拟机的执行引擎主要是用来执行java字节码,JVM的执行引擎执行字节码通过两种解释器执行的,字节码解释器与模板解释器,运行过程中,可能会触发即时编译(JIT),涉及到几种即时编译器,下面将分别进行介绍
解释器
- 解释器分为字节码解释器和模板解释器
字节码解释器
字节码解释器是解释执行的,所谓的解释执行,就是将java字节码转换成C++代码,在将C++代码编译成本地代码(硬编码),之所以会转成C++代码,是因为HotSpot虚拟机(
现目前虚拟机都指的是HotSpot虚拟机
)是由C++代码编写的,所以Java字节码指令的底层实现都是由C++代码实现,执行字节码指令其实就是对应的C++代码,而执行C++代码之前会将C++代码编译成本地代码,然后再执行所以,字节码解释器在工作的时候,他是逐行代码进行解释执行,准确的说是逐条字节码指令解释执行,由于字节码指令操作对象是栈帧中的操作数栈(伴随着入栈,出栈操作),因此我们常说字节码解释器是"基于栈的字节码解释执行引擎"
代码实例:
有如下java代码
public int add(){ int a = 10; int b = 20; return a + b; }
通过javap命令(或者Idea中的
jclasslib插件
),生成字节码如下:public int add(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, local=3, args_size=1 0:bipush 10 2:istore_1 3:bipush 20 5:istore_2 6:iload_1 7:iload_2 8:iadd 9:ireturn
由上面这段字节码可知,Java虚拟机会为这个add()方法分配深度为2的操作数栈和个数为3的局部变量表(Code属性中包含了操作数栈深度和局部变量表大小)
为什么局部变量表的大小是三个呢,程序中明明只有a,b两个局部变量?
- 是因为add()方法是非对象实例方法(方法)的局部变量表中索引为0的位置永远是this指针
- 内存模型为
上面的执行流程是这样:
- 0:bipush 10
- 将单字节的常量值(-128 - 127)推送到操作数栈顶,这里的10指的是参数,也就是将10压入操作数栈的栈顶
- 2:istore_1
- 将栈顶int型数值出栈,并存入局部变量表中索引为1的位置
- 3:bipush 20
- 将参数20推送到操作数据栈顶
- 5:istore_2
- 将栈顶int型数据值出栈并存入局部变量表中索引为2的位置
- 6:iload_1
- 从局部变量表中将索引为1的变量压入操作数栈中
- 7:iload_2
- 从局部变量表中将索引为2的变量压入操作数栈中
- 8:iadd
- 取出操作数栈中的数据并执行加法运算,并将返回值压入操作数栈中
- 9:ireturn
- 结束方法运行,并将栈顶整型元素返回给方法调用者
- 0:bipush 10
通过上述代码执行过程的分析可以看出,字节码解释器的执行过程就是逐条执行字节码指令
注意
:上面所说的逐条字节码指令进行解释执行,是指java虚拟机规范中给出的一种模型,实际上java虚拟机并不会按部就班的根据字节码指令逐条执行,因为虚拟机会对字节码指令做一系列优化,(如:指令重排),而且现在的虚拟机都是解释执行和编译执行混合模式,执行时的优化程度会更大,所以这里说的只是一种概念
模板解释器
- 模板解释器是编译执行的,他的执行过程是直接将java字节码编译成本地代码(硬编码),然后执行本地代码,相当于对于每一条字节码指令,都将他最后生成的本地代码给提交前写死了,JVM初始化的时候,就会直接将字节码指令对应的本地代码给加载到内存中,执行的时候直接执行对应的本地代码即可,这样就跳过了字节码转C++以及由C++编译成本低代码的过程,提高了执行速度。JVM通过def()函数为字节码指令创捷模板的,每个模板都会生成该指令对应的本地代码
三种运行模式
字节码解释器
- 早期JVM带用的就是该模式,字节码解释器模式下,逐条指令执行,代码运行速度相对较慢,因为他的编译条件相对宽松,编译所需要的信息较少,因此编译速度快,可以通过虚拟机参数-Xint设置
模板解释器
- 模板解释器执行速度很快,但是因为它需要将代码全部编译成本地代码,因此他便以需要收集的信息比较多,所以编译速度比较慢,可以通过虚拟机参数-Xcomp设置
字节码解释器 + 模板解释器
- 可以通过虚拟机参数-Xmixed设置,通过上面的分析,大家会觉得,既然模板解释器执行速度快,那JVM肯定会次啊用模板解释器来执行,其实并不是,在说明原因之前,需要先提一个概念:热点代码
三种运行模式性能比较
- 代码通过一段算术运算,分别设置参数:-Xint, -Xcomp, -Xmixed然后运行,可以看到-Xint耗时明显最多,所以他的执行性能最差,编译执行和混合执行模式在这段代码上性能差不多,如果当一个程序非常大的话,可能模板执行器模式耗时就会比混合模式更久些,因为编译需要的更长的时间
热点代码
热点代码
- 顾名思义,热点代码就是经常执行的代码,虚拟机会判断一个方法或者一个代码块的运行特别频繁,就会将这些代码认定为热点代码,热点代码主要包含两类代码:被多次调用的方法,被多次执行的循环体,多次调用的方法很好理解,多次执行的循环体是指一个方法内如果存在一个循环体,虽然这个方法可能只是被执行一次或者寥寥几次,但是这个循环体却被多次执行,主要体现在循环次数,那么这个循环体也是热点代码
热点探测
- 虚拟机是如何判断一段代码是不是热点代码呢,这就需要有一套热点探测的机制了,JVM主要有两种热点探测机制:
- 基于采样的热点探测
- 虚拟机定期检查各个线程的栈顶,如果发现某个方法频繁出入栈顶,就将该方法认定为热点代码
- 基于计数器的热点探测
- 为每个代码块(方法或者循环体)维护一个计数器,每执行一次代码快,计数器+1,当计数器值达到阈值时,就认定该代码块为热点代码,HotSpot采用的就是这种方式,Client编译器模式下,该阈值是1500;Server编译器模式下,该阈值是10000,也可以通过虚拟机参数-XX:CompileThreshold设定,不过热点代码的地位不是一直保持热度的,当该代码块一段时间没有执行或者执行次数无法达到阈值时,热度就会衰减一半,比如某个热点代码快的热点值是10000,热度衰减后就变成了5000
- 基于采样的热点探测
热点代码缓存
热点代码缓存在方法区中的一块内存空间,这块内存空间叫Code Cache,可以通过命令java-client-XX: +PrintFlagsFinal -version | grep CodeCache 查看Code Cache 默认大小
介绍了热点代码之后,再来说说现在的JVM为什么采用混合执行模式,试想一下,假如采用了模板解释器模式,不管三七二十一,直接对所有的代码都编译成硬编码,可能大部分代码都只是执行一次或者少数几次,这样编译全部代码就造成了性能浪费,而且对于现在动辄几百兆甚至几个G的应用,如果采用纯模板解释器,应用的启动就会很慢,可能启动后很长时间都无法使用,这也无法接受,因此采用混合模式,在应用启动后的前期,使用字节码解释器解释执行,当程序与逆行了一段时间之后,JVM就能够根据代码块的执行次数判断哪些代码属于热点代码,就会将热点代码博爱村在方法区,下次执行的时候就直接使用模板解释器编译执行,就无需在编译了,从而提升了性能,现在的虚拟机默认采用的混合模式,可以通过java -version 命令查看:
即时编译器
- 前面介绍的模板解释器所执行的本地代码就是即时编译器生成的,因此所谓的混合运行模式(-Xmixed)其实就是字节码解释器和即时编译器混合使用的方式,所以当使用解释执行模式时(-Xint),则只使用了字节码解释器,即时编译器并没有用;当使用编译模式执行时,这时候即时编译器就会起到作用(编译后的本地代码给模板解释器使用)。常见的即时编译器主要有两钟:C1编译器(客户端编译器)和C2编译器(服务端编译器),当虚拟机采用Client模式工作时,使用的是C1即时编译器,采用Server模式工作室,使用的是C2即使编译器,默认为Server模式,见上文中java-version执行结果的截图,也可以使用-client和-server参数设置
C1编译器
- Client模式下的即时编译器,他出发的条件相对宽松,需要收集的信息较少,他会对字节码在编译的时候进行浅程度的优化,因此相对于C2编译器,他的编译速度更快,但是执行速度相对较慢
C2编译器
- Server模式下的即使编译器,触发的条件比较苛刻,他会对代码进行更深层次的优化,因此他的编译程度比较深,编译后的代码质量更高,随之带来的也是更高的编译耗时,但是他的执行速度更快
即时编译器触发条件
上面其实已经在热点代码中介绍过了,当方法执行次数达到指定的阈值时,就会触发即时编译,同时也可以指定,即时编译器编译的其实就是热点代码,只有存在热点代码时,即时编译器才会起作用
当虚拟机执行到一个方法的时候,会先判断该方法是不是已经编译过(判断是否存在给i方法对应的本地代码),如果存在,则直接执行该本地代码,如果不存在,才会将该方法的热带你计数器+1,然后判断是否达到设定的阈值,一旦达到了阈值,则会触发即时编译器进行编译,所以当一个代码块已淡出法国即时编译,那么以后每次执行他都是执行他被编译后的本地机器码,也即采用得是编译执行模式(-Xcomp)
需要注意的是,当即时编译器进行编译时,执行引擎并不会等待其编译完成,而是直接使用字节码解释器解释执行,当即时编译器编译完成后,下一次在执行该方法的时候就会执行已经编译后的本地代码了,这样做的目的就是尽可能的提高执行效率,防止同步等待造成的性能浪费
即时编译器如何实现的
如果触发了即时编译,那么就会产生一个即时编译任务,将这个任务放到队列里,然后由VM_THREAD线程(linux系统)从这个队列里取出任务执行,而这个VM_THREAD线程可能会有多个,可以通过命名java -client -XX:+PrintFlagsFinal -version | grep ClCompilerCount 查看有多少个线程(默认4个),可以通过参数-XX:CICompilerCount=xxx进行设置,从而对即时编译器进行调优
混合编译
- 由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化成都越高的代码,所花费的时间便会越长,而且想要编译出来优化成都更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释器执行阶段的速度也有影响,为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机在编译子系统中加入了分层编译的功能(即混合编译),混合编译根据编译器编译,优化的规模与耗时,划分出来不同的编译层次,其中包括:
- 第0层:程序春节时执行,并且执行器不开启性能监控功能(Profiling)
- 第1层:使用客户端编译器将字节码编译成为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能
- 第2层:仍然使用客户端编译器执行,今开其方法及汇编次数统计等有限的性能监控功能
- 第3层:仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转,虚方法调用版本等全部的统计信息
- 第4层:使用服务端编译器将字节码编译为本地代码,相比客户端编译器,服务端编译器就会启用更多的编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化
javaC编译器与即时编译器
- 本文所说的"编译"都是指即时编译器的编译过程,它与javaC将java代码编译成自己阿尔玛是完全不同的,java编译器是由java代码实现,是java虚拟机之外的编译器,又称前端编译器;二即时编译器完全就是在java虚拟机内部运行,由C++代码实现,又称为后端编译器,他主要是运行时将字节码编译成本地机器码,由于java成需要运行的话,需要先经过javaC将.java文件编译成为.class文件,这个过程在迅即外部进行,然后由执行引擎执行字节码,而执行引擎执行字节码主要是由解释器+即使编译器搭配进行,这个过程在虚拟机内部进行,基于此,我们常说的"java是半编译半解释型语言"。