编译过程又可以分成两个阶段:编译和汇编。
编译过程:是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码
汇编过程:实际上指把汇编语言代码翻译成目标机器指令的过程。
从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
1) 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
2) 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
3) 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图中的各个步骤。
过程一:javac.exe的执行:Java代码编译是由Java源码编译器来完成,流程图如下所示:
过程二:java.exe的执行:Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:
HotSpot VM是目前市面上高性能虚拟机的代表作之一。它采用解释器与即时编译器并存的架构。
事实上,计算机并不认识高级语言,在执行过程中我们会把高级语言转换成计算机所能理解的一种中间格式(如汇编语言),然后才能理解计算机如何解释和执行这些中间的程序,以及系统的哪一部分影响程序的执行效率。
JVM设计者们的初衷仅仅只是单纯地为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释器在运行时采用逐行解释字节码执行程序的想法。
什么是解释器(Interpreter)?
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
工作机制(或工作任务)
解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。从这个角度说,java是解释语言。
当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
解释器分类
在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器、现在普遍使用的模板解释器。
只用解释器的问题
Java代码的执行分类
在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
问题:为什么说Java是半编译半解释型语言?
JDK1.0时代,将Java语言定位为“解释执行”还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。现在JVM在执行Java代码的时候,通常都会将解释执行与编译执行二者结合起来进行。
什么是JIT编译器?
JIT(Just In Time Compiler)编译器:就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
为什么还保留解释器执行方式?
有些开发人员会感觉到诧异,既然HotSpot VM中已经内置JIT编译器了,那么为什么还需要再使用解释器来“拖累”程序的执行性能呢?比如JRockit VM内部就不包含解释器,字节码全部都依靠即时编译器编译后执行。
首先明确:当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。
所以:尽管JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。
HotSpot JVM执行方式
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
案例:注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载要大于冷机状态。如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死。
在生产环境发布过程中,以分批的方式进行发布,根据机器数量划分成多个批次,每个批次的机器数至多占到整个集群的1/8。曾经有这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成分为两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前l/2发布成功的服务器马上全部宕机,此故障说明了JIT 的存在。—— 阿里团队
如何选择?
热点代码及探测方式:当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为“热点代码”,JIT编译器在运行时会针对那些频繁被调用的“热点代码”做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
方法调用计数器
热度衰减
回边计数器
它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为 “回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发 OSR 编译。(On-Stack Replacement)
OSR 实际上是一种技术,它指的是在程序执行过程中,动态地替换掉 Java 方法栈桢,从而使得程序能够在非方法入口处进行解释执行和编译后的代码之间的切换。事实上,去优化(deoptimization)采用的技术也可以称之为 OSR。
在不启用分层编译的情况下,触发 OSR 编译的阈值是由参数 -XX:CompileThreshold 指定的阈值的倍数。
该倍数的计算方法为:(OnStackReplacePercentage - InterpreterProfilePercentage)/100
其中 -XX:InterpreterProfilePercentage 的默认值为 33,当使用 C1 时 -XX:OnStackReplacePercentage 为 933,当使用 C2 时为 140。
也就是说,默认情况下,C1 的 OSR 编译的阈值为 13500,而 C2 的为 10700。在启用分层编译的情况下,触发 OSR 编译的阈值则是由参数 -XX:TierXBackEdgeThreshold 指定的阈值乘以系数。OSR 编译在正常的应用程序中并不多见。它只在基准测试时比较常见,因此并不需要过多了解。
Complier 与 Interpreter即:编译器与解释器
对比项 |
编译器 |
解释器 |
机器执行速度 |
快,因为源代码只需被转换一次 |
慢,因为每行代码都需要被解释执行 |
开发效率 |
慢,因为需要耗费大量时间编译 |
快,无需花费时间生成目标代码,更快的开发和测试 |
调试 |
难以调试编译器生成的目标代码 |
容易调试源代码,因为解释器一行一行地执行 |
可移植性(跨平台) |
不同平台需要重新编译目标平台代码 |
同一份源码可以跨平台执行,因为每个平台会开发对应的解释器 |
学习难度 |
相对较高,需要了解源代码、编译器以及目标机器的知识 |
相对较低,无需了解机器的细节 |
错误检查 |
编译器可以在编译代码时检查错误 |
解释器只能在执行代码时检查错误 |
运行时增强 |
无 |
可以动态增强 |
面试题
你是怎么指定JVM启动模式?(字节跳动)
那你知道-server和-client的区别吗?(美图)
缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。如下所示:
在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。Client Compiler注重启动速度和局部的优化;Server Compiler更加关注全局优化,性能更好,但由于会进行更多的全局分析,所以启动速度会慢。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
由-XX:+RewriteFrequentPairs参数控制。client模式默认关闭,server模式默认开启。
分层编译(Tiered Compilation)策略:程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
不过在Java7版本之后,一旦开发人员在程序中显式指定命令“-server”时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。
C1和C2编译器不同的优化策略:
总结:
Java 7开始引入了分层编译(Tiered Compiler)的概念,它结合了C1和C2的优势,追求启动速度和峰值性能的一个平衡。分层编译将JVM的执行状态分为了五个层次。五个层级分别是:
profiling就是收集能够反映程序执行状态的数据。其中最基本的统计数据就是方法的调用次数,以及循环回边的执行次数。
总的来说,C1的编译速度更快,C2的编译质量更高,分层编译的不同编译路径,也就是JVM根据当前服务的运行情况来寻找当前服务的最佳平衡点的一个过程。从JDK 8开始,JVM默认开启分层编译。
-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler去激活,才可以使用。
还需要继续优化中,最初只支持Linux x64 java base
JIT |
AOT |
1.具备实时调整能力 |
1.速度快,优化了运行时编译时间和内存消耗 |
1.运行期边编译速度慢 |
1.程序第一次编译占用时间长 |
在 OpenJDK 的官方 Wiki 上,介绍了HotSpot 虚拟机一个相对比较全面的、即时编译器(JIT)中采用的优化技术列表。
可使用:-XX:+PrintCompilation 打印JIT编译信息
动态编译(compile during run-time),英文称Dynamic compilation;Just In Time也是这个意思。
HotSpot对bytecode的编译不是在程序运行前编译的,而是在程序运行过程中编译的。
HotSpot里运行着一个监视器(Profile Monitor),用来监视程序的运行状况。
Java字节码(class文件)是以解释的方式被加载到虚拟机中(默认启动时解释执行)。 程序运行过程中,那一部分运用频率大,那些对程序的性能影响重要。对程序运行效率影响大的代码,称为热点(hotspot),HotSpot会把这些热点动态地编译成机器码(native code),同时对机器码进行优化,从而提高运行效率。对那些较少运行的代码,HotSpot就不会把他们编译。
HotSpot对字节码有三层处理:
不编译(字节码加载到虚拟机中时的状态。也就是当虚拟机执行的时候再编译);
编译(把字节码编译成本地代码。虚拟机执行的时候已经编译好了,不要再编译了);
编译并优化(不但把字节码编译成本地代码,而且还进行了优化)。
至于哪些程序那些不编译,那些编译,那些优化,则是由监视器(Profile Monitor)决定。
为什么字节码在装载到虚拟机之前就编译成本地代码呢?
动态编译器在许多方面比静态编译器优越。静态编译器通常很难准确预知程序运行过程中究竟什么部分最需要优化。
函数调用都是很浪费系统时间的,因为有许多进栈出栈操作。因此有一种优化办法,就是把原来的函数调用,通过编译器的编译,改成非函数调用,把函数代码直接嵌到调用出,变成顺序执行。
面向对象的语言支持多态,静态编译无效确定程序调用哪个方法,因为多态是在程序运行中确定调用哪个方法。
建议阅读:
热点代码:调用次数非常多的代码