运行期 【笔记】深入理解 Java 虚拟机:晚期优化( 五 )


为了解决虚方法的内联问题,Java 虚拟机引入了一种称为“类型继承关系分析”(Class,CHA)的技术,这是一种基于整个查询的类型分析技术,它用于确定在目前已加载的类中,某个接口是否有多于一种实现,某个类是否存在子类、子类是否为抽象类等信息 。
如果通过 CHA 分析得知某个方法只有一个版本,就可以进行内联,不过这种内联属于“激进优化”,需要预留一个“逃生门”,称为守护内联 。如果程序在执行过程中,虚拟机一直没有加载到令这个类继承关系发生变化的类,那这个内联优化的代码就可以一直使用下去 。但如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译的代码,返回到解释状态执行,或者重新进行编译 。
如果 CHA 查出来某方法有多个版本,则编译器还会进行最后一次努力,使用内联缓存Cache 来完成方法内联,这是一个建立在目标方法正常入口之前的缓存,其工作原理是:在未发生方法调用之前,内联缓存为空,当第一次调用发生后,缓存记录下方法接收者的版本信息 。后续每次执行都检查版本,一致则使用内联缓存,否则查找虚方法表进行方法分派 。
逃逸分析
逃逸分析是目前 Java 虚拟机中比较前言的优化技术,它与类型继承关系分析一样,并不是直接优化代码的技术,而是为其他优化手段提供依据的分析技术 。逃逸分析的基本行为是分析对象动态作用域:当一个对象在方法里定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸 。甚至还有可能被外部方法访问到,譬如赋值给类变量或其他线程中访问的实例变量,称为线程逃逸 。
如果能证明一个对象不会逃逸到方法或线程之外,就可以为这个变量进行一些高效优化:
栈上分配:Java 一般是在堆上分配对象的,对象的回收依赖虚拟机的垃圾收集系统,垃圾收集系统回收和整理内存都需要耗费时间 。如果一个对象不会逃逸出方法之外,那让这个对象在栈上分配会是一个不错的主意,对象所占用的内存空间可以随着栈帧出栈而销毁,减轻了垃圾收集系统的压力 。同步消除:线程同步本身是一个相对耗时的过程,如果逃逸线程分析确定一个对象不会逃逸出线程,不会被其他线程访问,那这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以消除掉 。标量替换:标量是指一个数据已经无法再分解成更小的数据了,Java 中的原始数据类型都不能再进一步分解,就可以称为标量 。相对的,如果一个数据可以继续分解,就可以称作聚合量,Java 中的对象就是典型的聚合量 。如果把一个 Java 对象拆散,根据程序访问情况,将其使用到的成员变量恢复原始类型来访问就叫做标量替换 。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,那程序真正执行的时候就可能不创建这个对象,改为直接创建它的若干个成员变量来代替 。将对象拆分后,除了可以让成员变量在栈上分配和读写外,还可以为后续进一步优化创建条件 。
下面给出一个演示逃逸分析的代码示例:
public class Test {public static void main(String[] args) throws Exception {long sum = 0;long count = 1000000;//warm upfor (long i = 0; i < count ; i++) {sum += fn(i);}Thread.sleep(500);for (long i = 0; i < count ; i++) {sum += fn(i);}System.out.prlongln(sum);System.in.read();}private static long fn(long age) {User user = new User(age);long i = user.getAge();return i;}}class User {private final long age;public User(long age) {this.age = age;}public long getAge() {return age;}}