JVM 即时编译器

Categories: 编程

[toc]

前言

Java 中有 2 种编译:

  • ` 前端编译 `:.java 文件 → .class 文件
  • ` 运行时编译 :运行时将 字节码→机器码 ,通过 JIT 解释器 `

本文分析运行时编译器,如何实现对 Java 代码的优化。

类编译加载执行过程

Java 从编译到运行的过程如下:

类编译

就是前面所说的 .java 文件 → .class 文件,可以通过 javac 命令完成。

类加载

当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。

类连接

验证

验证类符合 Java 规范和 JVM 规范,在保证符合规范的前提下,避免危害虚拟机安全。

准备

为类的静态变量分配内存,初始化为系统的初始值。对于 final static 修饰的变量,直接赋值为用户的定义值。例如,private final static int value=123,会在准备阶段分配内存,并初始化值为 123,而如果是 private static int value=123,这个阶段 value 的值 ` 仍然为 0`。

解析

` 将符号引用转为直接引用 ` 的过程。在编译时,Java 类并 ` 不知道所引用的类的实际地址 `,因此只能使用符号引用来代替。类结构文件的常量池中存储了符号引用,包括类和接口的全限定名、类引用、方法引用以及成员变量引用等。如果要使用这些类和方法,就需要把它们转化为 JVM 可以直接获取的内存地址或指针,即直接引用。

类初始化

VM 首先将执行构造器 <clinit> 方法,编译器会在将 .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 并没有实现栈上分配。
  • 锁消除
  • 标量替换:如果对象不被外部访问,且这个对象可以被拆分,则可以不创建这个对象,而是直接创建它的成员变量来替代。对象拆分后,对象的成员变量在栈或寄存器上,原对象无需内存空间。
Read More

深入分析 Java、Kotlin、Go 的线程和协程

【2020-12-10】