《深入理解Java虚拟机》学习笔记(6)--程序编译与代码优化

早期(编译期)优化
Javac这类编译器对代码的运行效率几乎没有任何优化措施。对性能的优化集中到了后端的即时编译器中,这样可以让那些不是由Javac产生的Class文件也同样能享受到编译器优化所带来的好处。

Javac做了许多针对Java语言编码过程的优化措施来改善程序员的编码风格和提高编码效率。相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持。

Javac编译过程大致可以分为3个过程:
(1)解析与填充符号表过程
(2)插入式注解处理器的注解处理过程
(3)语义分析与字节码生成过程
解析过程包括 词法分析语法分析两个过程。词法分析就是将源代码的字符流转变为标记(token)集合。 语法分析就是将token序列构造抽象语法树。

Java中的语法糖
泛型与类型擦除
泛型本质是参数化类型的应用,也就是操作的数据类型被指定为一个参数。可以应用到类、接口、方法上。泛型其实是javac提供给我们的一颗语法糖。Java中的泛型只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的裸类型了,并且在相应的地方插入 强制类型转换代码。
自动装箱、拆箱与遍历循环(Foreach循环)
条件编译
if (true) {
// 代码块A
} else {
// 代码块B
}
编译器会把代码块B的代码消除掉
晚期(运行期)优化
java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁时,就把这些代码认定为“热点代码”,为了提高热点代码的执行效率,运行时,虚拟机把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器成为 即时编译器 (JIT编译器)。 

hotspot和j9都是解释器和编译器并存的架构,其原因是,当程序需要迅速启动和执行的时候,可以用解释器解释执行,这样可以省去编译时间,加快启动时间,立即执行。当程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,以获取更高的执行效率。当运行环境中内存资源限制较大时,解释器可以节约内存,解释器还可以作为激进优化的编译器的“逃生门”(称为逆优化Deoptimization),即当激进优化的假设不成立时,退回到解释状态继续执行。在整个虚拟机架构中,解释器和编译器经常配合工作,如下图所示:

hotspot内置了两个即时编译器,client compiler和server compiler,称为C1,C2(client compiler获取更高的编译速度,server compiler来获取更好的编译质量),默认是采用解释器与其中一个编译器直接配合的方式工作,程序使用哪个编译器,取决于虚拟机运行的模式,HOTSPOT会根据自身版本和宿主机器的性能自动选择运行模式,用户也可以使用-client或-server去强制虚拟机运行在Client或Server模式。这种解释器编译器搭配的方式称为混合模式,用户还可以使用-Xint强制虚拟机使用“解释模式”,也可以使用-Xcomp强制“编译模式”。

由于即时编译本地代码需要占用程序运行时间, 想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响,为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启动分层编译的策略:
第0层,程序解释运行,不开启性能监控;
第1层,C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要则加入性能监控;
第2层,C2编译,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。
编译对象与触发条件
被JIT编译的热点代码有两类
  • 被多次调用的方法
  • 被多次执行的循环体;
对于前者编译器会以整个方法作为编译对象,属于标准的JIT编译方式;对于后者尽管编译动作是由循环体所触发的,但编译器依然会以整个方法作为编译对象,这种编译方式称之为栈上替换(OSR编译)
热点探测(判断一段代码是不是热点代码)的方式
  • 基于采样的热点探测:JVM会周期性检查各个线程的栈顶,如果某个方法经常出现在栈顶,那就认定为热点方法。简单高效,精度不够。 
  • 基于计数器的热点探测:统计方法执行次数。(hotspot使用这种方式)
编译优化技术
公共子表达式消除
如果一个表达式之前已经计算过了,并且参与者期间都没有发生变化,那该表达式就是公共子表达式,只需计算一次即可,第二次出现时不需要再次计算。
数组边界检查消除
java是动态安全的,访问数组前会先判断是否越界,若越界则抛出异常,但每次运行都判断,也是种性能负担。编译器在编译期间如果确定对数组的访问不会越界,则省略判断,这样运行时就可以提高效率。还有一种处理思路,即隐式异常处理,即先不做判断,而是等出了异常再处理。对于空指针异常、除0异常,通常采用这种方式来优化。对于数组极少越界(或者指针极少为空)的情况,这样能提升效率,但如果经常抛出异常,隐式异常优化反而会让程序更慢。 
方法内联
除了消除方法调用的成本外,更重要的意义是为其他优化手段建立良好的基础。

用invokespecial指令调用的私有方法、实例构造器、父类方法以及使用invokestatic指令进行调用的静态方法是在编译期进行解析,这些方法可以直接内联。除了这些方法外(Java中默认的实例方法为虚方法),在运行期要进行方法接受者的多态选择,因此无法在编译期确定应该使用哪个版本,也就无法内联。为了解决虚方法的内联问题,引入了类型继承关系分析(CHA)技术和内联缓存(Inline Cache)来完成方法内联。

在许多情况下虚拟机进行的内联都是一种激进优化,需要预留逃生门。
逃逸分析
逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用(方法逃逸),甚至还可能被外部线程所访问到(线程逃逸);如果能证明一个对象不会逃逸到方法或线程之外,则可能为这个变量进行一些高效的优化,比如栈上分配(减轻垃圾收集的压力)、同步消除(读写不会有竞争)、标量替换:
  • 栈上分配:将对象分配在栈中而不是堆中,可以省去垃圾回收的开销。
  • 同步消除:线程同步本身就是一个相对耗时的过程,若确定变量不会逃逸出线程,对这个变量就不需要实施同步措施 ,对这个变量实施的同步描述就可以消除掉。
  • 标量替换:不能再拆分的基本类型就是标量,对象就是聚合量。如果一个对象不会被外部访问,那将可能不创建对象,而是用组成他的一些标量的集合代替它。拆分后,不仅可以让标量在栈上,还可以为后续优化做基础。

你可能感兴趣的:(java)