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): 优化的范围涵盖了多个基本块

11.4.5 数组边界检查消除(Array Bounds Checking Elimination)