JVM笔记 - 程序编译与代码优化(晚期(运行期)优化)

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》笔记

1、概述

即时编译器并不是虚拟机必需的部分。

本章提及的编译器、即时编译器都是指 HotSpot 虚拟机内的即时编译器,虚拟机也是特指 HotSpot 虚拟机。

2、HotSpot虚拟机内的即时编译器

2.1、解释器与编译器

HotSpot 虚拟机中内置了两个即时编译器,分别称为 Client Compiler 和 Server Compiler。

HotSpot 虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,用户也可以使用”- client” 或”- server” 参数去强制指定虚拟机运行在 Client 模式或 Server 模式。

参数”- Xint” 强制虚拟机运行于“解释模式”( Interpreted Mode)。

参数”- Xcomp” 强制虚拟机运行于“编译模式”( Compiled Mode),这时将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。

为了在程序启动响应速度与运行效率之间达到最佳平衡, HotSpot 虚拟机还会逐渐启用分层编译( Tiered Compilation)[ 4] 的策略。

实施分层编译后, Client Compiler 和 Server Compiler 将会同时工作,许多代码都可能会被多次编译,用 Client Compiler 获取更高的编译速度,用 Server Compiler 来获取更好的编译质量,在解释执行的时候也无须再承担收集性能监控信息的任务。

2.2、编译对象与触发条件

“热点代码”有两类,即:被多次调用的方法。被多次执行的循环体。

这种编译方式因为编译发生在方法执行过程之中,因此形象地称之为栈上替换( On Stack Replacement, 简称为 OSR 编译,即方法栈帧还在栈上,方法就被替换了)。

判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测。

目前主要的热点探测判定方式有两种:基于采样的热点探测,基于计数器的热点探测。

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

当计数器超过阈值溢出了,就会触发 JIT 编译。

当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的。

使用虚拟机参数- XX:- UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数。

使用- XX: CounterHalfLifeTime 参数设置半衰周期的时间,单位是秒。

回边计数器,它的作用是统计一个方法中循环体代码执行的次数。

建立回边计数器统计的目的就是为了触发 OSR 编译。

参数- XX: OnStackReplacePercentage 来间接调整回边计数器的阈值。

2.3、编译过程

在默认设置下,无论是方法调用产生的即时编译请求,还是 OSR 编译请求,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。

用户可以通过参数- XX:- BackgroundCompilation 来禁止后台编译。

对于 Client Compiler 来说,它是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。

而 Server Compiler 则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到 GNU C++编译器使用-O2参数时的优化强度。

2.4、查看及分析即时编译结果

3、编译优化技术

3.1、优化技术概览

这些代码优化变换是建立在代码的某种中间表示或机器码之上,绝不是建立在Java源码之上的。

3.2、公共子表达式消除

3.3、数组边界检查消除

除了如数组边界检查优化这种尽可能把运行期检查提到编译器完成的思路之外,另外还有一种避免思路:隐式异常处理。

当 foo 不为空的时候,对 value 的访问是不会额外消耗一次对 foo 判空的开销的。代价就是当 foo 真的为空时,必须转入到异常处理器中恢复并抛出 NullPointException异常,这个过程必须从用户态转动内核态中处理,结束后再回到用户态,速度远比一次判空检查慢。

3.4、方法内联

只有使用invokespecial指令调用的私有方法、实例构造器、父类方法以及使用invokestatic指令进行调用的静态方法才是在编译期进行解析的。

3.5、逃逸分析

逃逸分析的基本行为就是分析对象动态作用域。

如果确定一个方法不会逃逸出方法之外,那让整个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧而销毁。在一般应用中,不会逃逸的局部对象所占用的比例很大,如果能使用栈上分配,那大量的对象就会随着方法结束而自动销毁了,垃圾手机系统的压力将会小很多。

同步消除

标量替换

4、Java与C/C++的编译器对比

除了它们自身的API库实现得好坏之外,其余的比较就成了一场“拼编译器”和“拼输出代码质量”的游戏。

Java虚拟机的即时编译器与C/C++的静态优化编译器相比,可能会由于下列这些原因导致输出的本地代码有一些劣势:

即时编译器运行占用的是用户程序的运行时间

Java语言是动态的类型安全语言

Java语言中虽然没有virtual关键字,但是使用虚方法的频率却远远大于C/C++语言

Java语言是可以动态扩展的语言

Java语言中对象的内存分配都是在堆上进行的,只有方法中的局部变量才能在堆上分配

5、本章小结

arthinking wechat
欢迎关注itzhai公众号