--- date: 2020-12-12 --- [toc] # 前言 Java 中有 2 种编译: - ` 前端编译 `:.java 文件 → .class 文件 - ` 运行时编译 `:运行时将 ` 字节码→机器码 `,通过 `JIT` 或 ` 解释器 ` 本文分析运行时编译器,如何实现对 Java 代码的优化。 # 类编译加载执行过程 Java 从编译到运行的过程如下: ![](http://yano.oss-cn-beijing.aliyuncs.com/2020-12-12-101818.jpg) ## 类编译 就是前面所说的 .java 文件 → .class 文件,可以通过 `javac` 命令完成。 ## 类加载 当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。 ## 类连接 ### 验证 验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安全。 ### 准备 为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,直接赋值为用户的定义值。例如,`private final static int value=123`,会在准备阶段分配内存,并初始化值为 123,而如果是 `private static int value=123`,这个阶段 value 的值 ` 仍然为 0`。 ### 解析 ` 将符号引用转为直接引用 ` 的过程。在编译时,Java 类并 ` 不知道所引用的类的实际地址 `,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。 ### 类初始化 VM 首先将执行构造器 `` 方法,编译器会在将 .java 文件编译成 .class 文件时,收集所有 ` 类初始化代码 `,包括 ` 静态变量赋值语句 `、` 静态代码块 `、` 静态方法 `,收集在一起成为 `()` 方法。 # 即时编译 类在调用执行过程中,执行引擎会把 ` 字节码转为机器码 `,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译——即时编译。 在执行时,虚拟机首先会由 ` 解释器 ` 完成编译,当发现 ` 热点代码 ` 后,` 即时编译器(JIT)` 会把这些代码编译成 ` 本地平台相关的机器码 `,并进行各个层次的优化,保存到内存中。 ## 即时编译器类型 Java 8 有 2 个: - `C1 编译器(Client Compiler)`:简单快速的编译器,主要在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序。 - `C2 编译器(Server Compiler)`:适用于执行时间较长的程序。 Java 9 引入了 `AOT 编译器 `,可以在程序运行前进行静态编译,可以避免运行时的编译消耗和内存消耗,class 文件可以直接编译成 so 二进制文件。 Java 10 引入了 JIT 编译器 Graal,C1 编译器和 C2 编译器都是用 C++ 实现的,Graal 是使用 Java 实现的。 ## 热点探测 是 JIT 优化的条件,是基于计数器的,为每个方法建立计数器统计方法的执行次数,超过一定阈值就认为是 ` 热点方法 `。 有 2 种计数器: - ` 方法调用计数器(Invocation Counter)`:统计方法被调用的次数 - ` 回边计数器(Back Edge Counter)`:统计一个方法中 ` 循环体代码执行的次数 `,在字节码中遇到 ` 控制流向后跳转 ` 的指令称为 “回边”(Back Edge). 目的是为了触发 OSR(On StackReplacement)编译,即 ` 栈上编译 `,将热点代码编译成机器语言并缓存。 ## 编译优化技术 ### 方法内联 调用一个方法通常要经历压栈和出栈。调用方法是将程序执行顺序转移到存储该方法的内存地址,将方法的内容执行完后,再返回到执行该方法前的位置。 这种执行操作要求在执行前 ` 保护现场并记忆执行的地址 `,执行后要 ` 恢复现场 `,并 ` 按原来保存的地址继续执行 `。 方法调用会产生一定的时间和空间方面的开销。 对于那些方法体代码不大,又频繁调用的方法,可以使用方法内联,把目标方法的代码复制到发起调用的方法中,避免发生真实的方法调用。 ### 逃逸分析 逃逸分析(Escape Analysis)是判断一个对象 ` 是否被外部方法引用或外部线程访问 ` 的分析技术,编译器会根据逃逸分析的结果对代码进行优化。 - 栈上分配:对象默认是分配在堆中的,对象不再使用时,需要垃圾回收。如果逃逸分析一个对象仅在方法中使用,可以将对象分配在栈上,该对象可以随着方法的调用结束自动回收内存。Java 8 并没有实现栈上分配。 - 锁消除 - 标量替换:如果对象不被外部访问,且这个对象可以被拆分,则可以不创建这个对象,而是直接创建它的成员变量来替代。对象拆分后,对象的成员变量在栈或寄存器上,原对象无需内存空间。