第11章 后端编译与优化
文章目录
11.2 即时编译器
Java程序最初都是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁,就会把这些代码认定为“热点代码”(Hot Spot Code),虚拟机将会把这些代码编译成本地机器码,并以各种手段尽可能地进行代码优化,运行时完成这个任务的后端编译器被称为即时编译器
11.2.1 解释器与编译器
解释器
- 省去编译的时间,立即运行
- 节约内存
- 作为编译器激进优化时后备的逃生门
编译器
- 编译成本地代码,减少解释器的中间损耗,获得更高的执行效率
分层编译
- 第0层。程序纯解释执行,不开启性能监控功能(Profiling)
- 第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启性能监控功能
- 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能
- 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息
- 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化
11.2.2 编译对象与触发条件
热点代码(编译对象都是方法)
- 被多次调用的方法
- 被多次执行的循环体
- 栈上替换(On Stack Replacement,OSR): 编译时传入执行入口点字节码序号(Byte Code Index,BCI)
热点探测
- 基于采样的热点探测(Sample Based Hot Spot Code Detection)。周期性地检查各个线程的调用栈顶,如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”
- 实现简单高效,还可以很容易地获取方法调用关系
- 很难精确地确认一个方法的热度,容易受到线程阻塞或别的外界因素的影响
- 基于计数器的热点探测(Counter Based Hot Spot Code Detection)。为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是“热点方法”
- 实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系
- 统计结果相对来说更加精确严谨
计数器
- 方法调用计数器(Invocation Counter): 统计方法被调用的次数
- 热度的衰减(Counter Decay): 当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那该方法的调用计数器就会被减少一半,这段时间就称为此方法统计的半衰周期(Counter Half Life Time)
- 回边计数器(Back Edge Counter,“回边”的意思就是指在循环边界往回跳转)
11.2.3 编译过程
高级中间代码表示(High-Level Intermediate Representation,HIR,即与目标机器指令集无关的中间表示)。
静态单分配(Static Single Assignment,SSA)
低级中间代码表示(Low-Level IntermediateRepresentation,LIR,即与目标机器指令集相关的中间表示)
11.3 提前编译器
11.3.1 提前编译的优劣得失
11.4 编译器优化技术
11.4.2 方法内联
把目标方法的代码原封不动地“复制”到发起调用的方法之中,避免发生真实的方法调用
虚方法内联问题
- 类型继承关系分析(Class HierarchyAnalysis,CHA): 这是整个应用程序范围内的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息
- 守护内联(Guarded Inlining): 查询到只有一个版本
- 内联缓存(Inline Cache): 查询到多个版本,建立在目标方法正常入口之前的缓存,记录下方法接收者的版本信息
- 单态内联缓存(Monomorphic Inline Cache): 每次调用的方法接收者版本都是一样的
- 超多态内联缓存(Megamorphic Inline Cache): 方法接收者不一致
11.4.3 逃逸分析(Escape Analysis)
基本原理
- 方法逃逸: 当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中
- 线程逃逸: 被外部线程访问,譬如赋值给可以在其他线程中访问的实例变量
优化
- 栈上分配(Stack Allocations):如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存 空间就可以随栈帧出栈而销毁。支持方法逃逸,不支持线程逃逸
- 标量替换(Scalar Replacement):把一个Java对象拆散,根据程序访问的情况,将其用到的成员变量恢复为原始类型来访问。它不允许对象逃逸出方法范围内。
- 标量: 一个数据已经无法再分解成更小的数据来表示了(int、long等数值类型及reference类型等)
- 聚合量(Aggregate): 一个数据可以继续分解(对象)
- 同步消除(Synchronization Elimination):如果确定一个变量不会逃逸出线程,无法被其他线程访问,对这个变量实施的同步措施也就可以安全地消除掉。
11.4.4 公共子表达式消除
如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化
局部公共子表达式消除(Local Common Subexpression Elimination): 优化仅限于程序基本块内
全局公共子表达式消除(Global Common Subexpression Elimination): 优化的范围涵盖了多个基本块