JVM提前编译与即时编译

提前编译 | 即时编译

Posted by Booogu on February 25, 2021
1252 字 4 分钟

即时编译(JTT)

编译的对象与触发条件

  • 被多次调用的方法 —— 热点是整个方法,依靠方法调用触发编译,替换整个方法
  • 被多次执行的循环体(带着为了跑分软件的私心) —— 热点是一段代码,但也必须替换整个方法,触发栈上替换(On Stack Replacement OSR),即方法的栈帧还在栈上,方法就被替换了

如何判断执行多次

  • 基于采样的热点探测——周期性地检查各个线程地调用栈顶,发现”热点方法“。优点:简单高效; 缺点:很难精确确认热度,容易受线程阻塞等影响
  • 基于计数器的热点探测 —— 为每个方法(甚至代码块)建立计数器,统计方法的执行次数,超过阈值则判定热点。
    • 方法调用计数器:存在”衰减“,有半衰周期,默认统计的是”相对调用次数“即一段时间内的调用次数
    • 回边计数器(统计方法中循环体执行次数):没有衰减,统计的是绝对调用次数,而且还会通知方法计数器

提前编译(AOP)

优劣势分析

  • 劣势:提前编译不仅和目标机器相关,还必须与HotSpot虚拟机的运行时配合,所以会破坏平台无关性。存在字节膨胀
  • 优势:减少即时编译带来的程序运行时间占用与运算资源占用,主流有两条路:
    • 一种是在程序运行前把代码编译成机器码,完成静态翻译工作
    • 另一种是把原本即时编译器在运行时要做的编译工作提前做好并保存下来(本质是一种缓存思想,给即时编译器做缓存加速)

与JTP对比(不足之处)

  • 性能分析制导优化:静态时无法分析程序中抽象类时什么类型、条件判断时走哪个分支、方法调用时选哪个版本、循环调用多少次等,而即时编译则可以看出这些偏好性,可以把热代码集中放到一起,集中优化和分配更好的资源(分支预测、寄存器、缓存等)给它。
  • 激进预测性优化:即时编译可以在有兜底(退回低级编译器、甚至解释执行)的前提下,做一些高概率下不会出错的预测判断,往往可以大幅降低程序的复杂度,比如遇到虚方法,可以直接去做内联
  • 链接时优化:比如做一些跨越动态链接库的方法内联,静态编译无法做到

编译器优化技术(待深入学习)

比如循环变换、全局代码调整等

简单常用的几种

  • 方法内联:去除方法调用
  • 冗余访问消除:把不会影响执行结果的调用、访问等去掉
  • 复写传播:去掉没有必要使用的一个额外变量,直接用原有变量代替
  • 无用代码消除:去掉永远不会被执行的代码

最重要的几种

  • 最重要的优化技术之一:方法内联
  • 最前沿的优化技术之一:逃逸分析
  • 语言无关的经典优化技术:公共子表达式消除和数组边界检查消除

逃逸分析技术

逃逸分析:分析对象动态作用域,当一个对象在方法里被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至可能被外部线程访问到,这种称为线程逃逸。

如果证明一个对象不会逃逸到方法或线程之外,可能进行如下优化:

  • 栈上分配:在栈上分配内存,不再到堆上,这个只适用于方法逃逸,不能支持线程逃逸
  • 标量替换:把一个完整的Java对象拆散,根据程序访问的实际情况,将其用到的成员变量恢复为原始类型来访问,这个过程就是标量替换。不允许存在方法逃逸
  • 同步消除:如果逃逸分析后,确定没有线程逃逸,可以直接把同步操作消除掉。