Java虚拟机——后端编译与优化

  • 编译器无论在何时、何种状态下将Class文件转换成与本地基础设施相关的二进制机器码,它都可以视为整个编译过程的后端。
  • 即时编译一直是绝对主流的编译形式,不过提前编译也逐渐被主流JDK支持。

1 即时编译器

  • 目前两款主流的Java虚拟机(HotSpot、OpenJ9里面),Java程序都是通过解释器进行解释执行的。当虚拟机认为某个方法或代码块的运行特别频繁,就会把这些代码编译成本地机器码,运行时完成这个任务的编译器叫作即时编译器

1.1 解释器与编译器

  • 目前主流的Java虚拟机都采用了解释器与编译器并存的运行架构。
  • 当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译时间,立刻运行。当程序启动后,随着时间地推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器中间的损耗。
  • HotSpot虚拟机内置了两个(或三个)即时编译器,其中两个编译器存在以及。分别是客户端编译器(C1编译器)服务端编译器(C2编译器),第三个是JDk10时出现,为了代替C2的Graal编译器
  • 为了在程序启动相应速度与运行效率之间达到最佳平衡状态,HotSpot虚拟机在编译子系统中加入了分层编译的功能。
    Java虚拟机——后端编译与优化_第1张图片

分层编译的好处是什么?

  1. 实施分层编译后,解释器、客户端编译器、服务端编译器就会同时工作,热点代码可能被多次编译。
  2. 用客户端编译器可以获得更高的编译速度,用服务端编译器来获取更好的编译质量,在解释执行的时候也无须承担收集性能监控信息的任务。
    在这里插入图片描述

2 编译对象与触发条件

  • 本章概述中提到了在运行过程中会被即时编译器编译的目标是"热点代码",这里所指的热点代码主要有两类。
  1. 被多次调用的方法
  2. 被多次调用的循环体
  • 对于这两种情况,编译的目标对象都会是整个方法体,而不是单独的循环体。
    Java虚拟机——后端编译与优化_第2张图片

3 编译过程

  • 无论是方法调用产生的标准编译请求,还是栈上替换编译请求。虚拟机在编译器还未完成编译之前,都将按照解释方法继续执行代码。

  • 而编译动作会进行在后台的编译线程中。

  • 在后台编译执行的过程中,服务端编译器和客户端编译器的编译过程是有差别的。

  • 客户端编译器

  • 它是一个相对简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。
    Java虚拟机——后端编译与优化_第3张图片

  • 服务端编译器

  • 它是一个专门面向服务端典型用场景的、为服务端的性能配置针对性调整过 的 编译器。
    Java虚拟机——后端编译与优化_第4张图片

2 提前编译器

  • 提前编译的发展,直到在Android的世界里,使用了提前编译的ART,ART一诞生就把使用即时编译器的Dalvik虚拟机按在地上使劲蹂躏。

提前编译的优劣得失?

  • 优点
  1. 提前编译没有执行时间和资源限制的压力,能够毫无顾虑地使用重负载优化手段。
  • 缺点
  1. 最传统的提前编译应用形式,就是在程序运行之前把程序代码编译成机器码的静态翻译工作。但是它会占用程序运行时间和运算资源。
  2. 提前编译的第二条途径,就是将编译工作提前做好后保存下来,下次运行到这块代码的时候,就直接加载进来使用。但是这种即时编译缓存输出的代码质量反而要低于即时编译器。

3 编译器优化技术

  • 编译器的目标就是由程序代码翻译成本地机器码的工作
  • 但是它的难点不在于能不能成功翻译出机器码,而是输出代码优化质量的高低。
  • 即时编译器优化技术的数量有很多,接下来会介绍几种常用的优化技术。并且通过Java代码变化来展示,不过即时编译器对这些代码优化变化是建立在 代码的中间表示或者是机器码之上的。

基础案例

package Compilation;

public class BasicDemo {
    B b = new B();
    int y , z , sum;
    
    static class B{
        int value;
        final int get(){
            return value;
        }
    }
    
    public void foo(){
        y = b.get();
        //...
        z = b.get();
        sum = y + z;
    }
}
  1. 方法内联
  • 先采用方法内联。主要目的有两个,一个是去除方法调用的成本(如查找方法版本、建立栈帧等);二是为其他优化建立良好的基础。(编译器一般会把内联优化放在优化序列最靠前的位置)
public void foo(){
    y = b.value;
    //...
    z = b.value;
    sum = y + z;
}
  1. 冗杂访问消除
  • 假设代码中间的省略号所代表的操作,不会改变b.value的值。
  • 那么就可以把z = b.value 变成 z = y。(就可以不再去访问对象b的局部变量)
  • 如果把b.value看成是一个表达式,也可以看成一种公共子表达式消除。
//冗杂访问消除
public void foo(){
    y = b.value;
    //...
    z = y;
    sum = y + z;
}
  1. 复写传播
  • 逻辑中没有必要使用额外的变量z,它与变量y完全相同,所以可以用y来代替z。
//复写传播
public void foo(){
    y = b.value;
    //...
    y = y;
    sum = y + y;
}
  1. 无用代码消除
  • 完全没有意义的代码或者是永远不会被执行的代码也可以被删除。
public void foo(){
    y = b.value;
    //...
    sum = y + y;
}

3.1 方法内联

- 方法内联是编译器最重要的优化手段

  • 他可以消除方法调用的成本,也可以为其他优化手段建立基础。
public static void foo(Object object){
    if(object != null){
        System.out.println("do something");
    }
}
public static void testInline(String[] args){
    Object obj = null;
    foo(obj);
}
  • 例子中的testInline里面的代码都是无用代码,但是如果不做内联的话,就无法消除这些无用代码。

为什么说方法内联的过程不容易?

  1. 因为除了非虚方法以外,虚方法调用都必须在运行时进行方法接收者的多态选择,它们可能存在多余一个版本的方法接收者。Java中默认的实例方法都是虚方法
  2. 对于虚方法,编译器静态地去做内联的时候很难确定使用哪个版本

3.2 逃逸分析

  • 基本原理:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中。(方法逃逸)。甚至还有可能被外部线程访问到,例如赋值给可以在其他线程中访问的实例变量。(线程逃逸)
  • 如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度较低,则能为这个对象实例采取不同程度的优化。(例如可以将这个对象从堆上分配,改成在栈上分配,对象所占用的内存空间就会栈帧出栈而销毁。)

3.3 公共子表达式消除

  • 如果一个表达式E已经被计算过了,并且从先前计算到现在E中所有变量没有发生变化,那么E这次出现成为公共子表达式
    - 可以直接用前面计算过的表达式结果代替E。

你可能感兴趣的:(Java虚拟机,java,开发语言)