本文共 6813 字,大约阅读时间需要 22 分钟。
在部分的商用虚拟机中,Java 程序最初是通过解释器(Interpreter )进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,即时编译器(Just In Time Compiler )会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化。
1.解释器和编译器各有各的优点:
解释器优点:当程序需要迅速启动的时候,解释器可以首先发挥作用,省去了编译的时间,立即执行。解释执行占用更小的内存空间。同时,当编译器进行的激进优化失败的时候,还可以进行逆优化来恢复到解释执行的状态。
编译器优点:在程序运行时,随着时间的推移,编译器逐渐发挥作用根据热点探测功能,,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
因此,整个虚拟机执行架构中,解释器与编译器经常配合工作,如下图所示。HotSpot中内置了两个即时编译器,分别称为 Client Compiler和 Server Compiler ,或者简称为 C1编译器和 C2编译器。目前的 HotSpot编译器默认的是解释器和其中一个即时编译器配合的方式工作,具体是哪一个编译器,取决于虚拟机运行的模式,HotSpot虚拟机会根据自身版本与计算机的硬件性能自动选择运行模式,用户也可以使用 -client和 -server参数强制指定虚拟机运行在 Client模式或者 Server模式。这种配合使用的方式称为“混合模式”(Mixed Mode),用户可以使用参数 -Xint 强制虚拟机运行于 “解释模式”(Interpreted Mode),这时候编译器完全不介入工作。另外,使用 -Xcomp 强制虚拟机运行于 “编译模式”(Compiled Mode),这时候将优先采用编译方式执行,但是解释器仍然要在编译无法进行的情况下接入执行过程。通过虚拟机 -version 命令可以查看当前默认的运行模式。
1.在运行过程中会被即时编译的“热点代码”有两类,即:
默认情况下,无论是方法调用产生的即时编译请求,还是 OSR 请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行,用户可以通过参数 -XX:-BackgroundCompilation来禁止后台编译,这样,一旦达到 JIT的编译条件,执行线程向虚拟机提交便已请求之后便会一直等待,直到编译过程完成后再开始执行编译器输出的本地代码。
1.对于 Client 模式而言
**client compiler,又称C1编译器,较为轻量,只做少量性能开销比较高的优化,它占用内存较少,适合于桌面交互式应用。**在寄存器分配策略上,JDK6以后采用的为线性扫描寄存器分配算法,其他方面的优化,主要有方法内联、去虚拟化、冗余消除等。
它是一个简单快速的三段式编译器,主要关注点在于局部的优化,放弃了许多耗时较长的全局优化手段。
1. 第一阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion, HIR)。在此之前,编译器会在字节码上完成一部分基础优化,如 方法内联,常量传播等优化。 2. 第二阶段,一个平台相关的后端从 HIR 中产生低级中间代码表示(Low-Level Intermediate Representation,LIR),而在此之前会在HIR 上完成另外一些优化,如空值检查消除,范围检查消除等,让HIR更为高效。 3. 第三阶段,在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR 上分配寄存器,做窥孔(Peephole)优化,然后产生机器码。 Client Compiler 的大致执行过程如下图所示: A、方法内联 多个方法调用,执行时要经历多次参数传递,返回值传递及跳转等,C1采用方法内联,把调用到的方法的指令直接植入当前方法中。-XX:+PringInlining来查看方法内联信息,-XX:MaxInlineSize=35控制编译后文件大小。 B、去虚拟化 是指在装载class文件后,进行类层次的分析,如果发现类中的方法只提供一个实现类,那么对于调用了此方法的代码,也可以进行方法内联,从而提升执行的性能。 C、冗余消除 在编译时根据运行时状况进行代码折叠或消除。2.对于 Server Compiler 模式而言
Server compiler,称为C2编译器,较为重量,采用了大量传统编译优化的技巧来进行优化,占用内存相对多一些,适合服务器端的应用。和C1的不同主要在于寄存器分配策略及优化范围,寄存器分配策略上C2采用的为传统的图着色寄存器分配算法,由于C2会收集程序运行信息,因此其优化范围更多在于全局优化,不仅仅是一个方块的优化。收集的信息主要有:分支的跳转/不跳转的频率、某条指令上出现过的类型、是否出现过空值、是否出现过异常等。
它是专门面向服务端的典型应用,并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到 GNU C++编译器使用-O2 参数时的优化强度,它会执行所有的经典的优化动作,如无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块冲排序(Basic Block Reordering)等,还会实施一些与Java 语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、空值检查消除(Null Check Elimination ,不过并非所有的空值检查消除都是依赖编译器优化的,有一些是在代码运行过程中自动优化 了)等。另外,还可能根据解释器或Client Compiler提供的性能监控信息,进行一些不稳定的激进优化,如守护内联(Guarded Inlining)、分支频率预测(Branch Frequency Prediction)等。 Server Compiler 编译器可以充分利用某些处理器架构,如(RISC)上的大寄存器集合。从即时编译的角度来看,Server Compiler 无疑是比较缓慢的,但它的便以速度仍远远超过传统的静态优化编译器,而且它相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码的执行时间,从而抵消了额外的编译时间开销,所以也有很多非服务端的应用选择使用Server 模式的虚拟机运行。 逃逸分析是C2进行很多优化的基础,它根据运行状态来判断方法中的变量是否会被外部读取,如不会则认为此变量是不会逃逸的,那么在编译时会做标量替换、栈上分配和同步消除等优化。 方法逃逸+线程逃逸 1)方法逃逸:分析对象的动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递给其他方法,称为方法逃逸。 1)线程逃逸:甚至还有可能被外部线程访问到,比如赋值给类变量或可以在其他线程中访问到的实例变量,称为线程逃逸。 1. 标量替换 如果把一个java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换;那程序真正执行的时候,将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。 这样做的好处是如果创建的对象并未用到其中的全部变量,则可以节省一定的内存。对于代码执行而言,无需去找对象的引用,也会更快一些。 2. 栈上分配 如果point没有逃逸,那么C2会选择在栈上直接创建Point对象的实例,而不是在JVM堆上。在栈上分配的好处一方面是加快速度,另一方面是回收时随着方法的结束,对象被回收了。 3. 同步消除 如果发现同步的对象未逃逸,因为不存在其它线程参与该对象的竞争,那也就没有必要进行同步了,C2编译时会直接去掉同步。线程同步本身就是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施就可以消除掉。 C2还会基于拥有的运行信息来做其他优化,比如编译分支频率执行高的代码等。4.对于分层编译而言
Java7默认开启分层编译(tiered compilation)策略,由C1编译器和C2编译器相互协作共同来执行编译任务。C1编译器会对字节码进行简单和可靠的优化,以达到更快的编译速度;C2编译器会启动一些编译耗时更长的优化,以获取更好的编译质量。
(1)解释器不再收集运行状态信息,只用于启动并触发C1编译 (2)C1编译后生成带收集运行信息的代码 (3)C2编译,基于C1编译后代码收集的运行信息进行激进优化,当激进优化的假设不成立时,再退回使用C1编译的代码 解释器与编译器并存 如果选用完全解释策略,那么编译器将停止所有的工作,字节码将完全依靠解释器逐行解释执行。 如果选用完全编译策略,那么解释器仍然会在编译器无法进行的特殊情况下介入运行,这主要是确保程序能够最终顺序执行。 SunJDK之所以未选择在启动时即编译成机器码的原因如下: (1)静态编译并不能根据程序的运行状态来优化执行的代码,C2这种方式是根据运行状态来进行动态编译的,例如分支判断、逃逸分析等,这些措施会对提升程序执行的性能起到很大的帮助,在静态编译的情况下是无法实现的,给C2收集运行数据越长的时间,编译出来的代码会越优。 (2)解释执行比编译执行更节省内存 (3)启动时解释执行的启动速度比编译再启动更快。1.编译期优化(早期优化)
为了保证JRuby,Groovy等语言编译的字节码也能得到性能优化,JVM将性能优化放在了后期的运行时优化,即JIT运行时编译优化中。
具体优化: 1. 编译期优化主要为语法糖,用来实现Java的各种新的语法特性,比如泛型,变长参数,自动装箱/拆箱。 2 .Java语法糖:与字节码无关,编译后会去掉它们。作用仅仅为方便码农写代码,以及将运行时异常在编译期及早发现(如泛型的使用)。 * 泛型与类型擦除 Java泛型只在编译期存在,编译完成后的字节码中会替换为原生类型。故称Java泛型为伪泛型。C#的泛型在运行期仍然存在。 * 条件编译 if语句中使用常量。比如if(false) {},这个语句块不会被编译到字节码中.这个过程在编译时的控制流分析中完成。2.运行时优化(晚期优化)
1. 劣势
2. 优势