《码出高效:Java 开发手册》第 4 章 走进 JVM

4.1 字节码

Java 所有指令有 200 个左右,一个字节(8 位)可以存储 256 中不同的指令信息,一个这样的字节称为字节码Bytecode )。

JVM 在字节码上也设计了一套操作码助记符,使用特殊单词来标记这些数字。

字节码必须通过类加载过程加载到 JVM 环境后,才可以执行。执行有三种模式:

  1. 解释执行
  2. JIT 编译执行
  3. JIT 编译与解释混合执行(主流 JVM 默认执行模式)
    优势:
    • 解释器在启动时先解释执行,省去编译时间
    • 随着时间的推移,JVM 通过热点代码统计分析,识别高频的方法调用、循环体、公共模块等,基于强大的 JIT 动态编译技术,将热点代码转换成机器码,直接交给 CPU 执行。

JIT 的作用是将字节码动态地编译成可以直接发送给处理器指令执行的机器码。

主要的字节码指令:

  • 加载或存储指令
    • 将局部变量加载到操作栈
      • ILOAD : 将 int 类型的局部变量压入栈
      • ALOAD : 将对象引用的局部变量压入栈
    • 从操作栈顶存储到局部变量表
      • ISTOREASTORE
    • 将常量加载到操作栈顶(这是极为高频使用的指令)
      • ICONST : 加载 -1 ~ 5 的数
      • BIPUSH : 即 Byte Immediate PUSH,加载 -128 ~ 127 之间的数
      • SIPUSH : 即 Short Immediate PUSH,加载 -32768 ~ 32767 之间的数
      • LDC : 即 Load Constant,在 -2147483648 ~ 2147483647 或是字符串时,JVM 采用 LDC 指令压入栈中。
  • 运算指令:IADDIMUL
  • 类型转换指令:I2LD2F
  • 对象创建与访问指令
    • 创建对象指令:NEWNEWARRAY
    • 访问属性指令:GETFIELDPUTFIELDGETSTATIC
    • 检查实例类型指令:INSTANCEOFCHECKCAST
  • 操作栈管理指令
    • 出栈操作:POPPOP2
    • 复制栈顶元素并压入栈:DUP
  • 方法调用与返回指令
    • INVOKEVIRTUAL : 调用对象的实例方法
    • INVOKESPECIAL : 调用实例初始化方法、私有方法、父类方法等
    • INVOKESTATIC : 调用类静态方法
    • RETURN : 返回 VOID 类型
  • 同步指令
    • ACC_SYNCHRONIZED
    • MONITORENTER
    • MONITOREXIT
  • 其他指令
    • LINENUMBER : 存储了字节码与源码行号的对应关系
    • LOCALVARIABLE : 存储当前方法中使用到的局部变量表

源码转化成字节码的过程:

即时编译流程:

4.2 类加载过程

ClassLoader 提前加载 .class 文件到内存。在加载类时,使用的是 Parents Delegation Model ,译为双亲委派模型(译为溯源委派加载模型可能更加贴切)。

类加载过程:

  1. Load 阶段(加载
    读取类文件产生二进制流,并转化为特定的数据结构,初步校验 cafe babe 魔法值、常量池、文件长度、是否有父类等,然后创建对应类的 java.lang.Class 实例。
  2. Link 阶段(链接
    包括验证、准备、解析三个步骤:
    1. 验证:更详细的校验,比如 final 是否合规、类型是否正确、静态变量是否合理等;
    2. 准备:为静态变量分配内存,并设定默认值;
    3. 解析:解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。
  3. Init 阶段(初始化
    执行类构造器 <clinit> 方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马上解析另外一个类,在虚拟机栈中执行完毕后通过返回值进行赋值。

类加载是一个将 .class 字节码文件实例化成 Class 对象并进行相关初始化的过程。在这个过程中,JVM 会初始化继承树上还没有被初始化过的父类并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等

类加载器类似于原始部落结构,存在权力等级制度。

  1. 最高的一层是 Bootstrap ,它是在 JVM 启动时创建的,通常由与操作系统相关的本地代码实现,是最根基的类加载器,负责装载最核心的 Java 类,比如 ObjectSystemString 等。
  2. 第二层是在 JDK 9 版本中,称为 Platform ClassLoader ,即平台类加载器,用以加载一些扩展的系统类,比如 XML、加密、压缩相关的功能类等,在 JDK 9 之前的加载器是 Extension ClassLoader ;
  3. 第三层是 Application ClassLoader 的应用类加载器,主要是加载用户定义的 CLASSPATH 路径下的类。
public class ClassLoaderTest {
    @Test
    public void show_class_loader() {

        // JDK 版本:CORRETTO 17.0.6

        // 正在使用的类加载器是 AppClassLoader
        ClassLoader c = ClassLoaderTest.class.getClassLoader();
        // jdk.internal.loader.ClassLoaders$AppClassLoader@70dea4e
        System.out.println(c);

        // AppClassLoader 的父加载器是 PlatformClassLoader
        ClassLoader c1 = c.getParent();
        // jdk.internal.loader.ClassLoaders$PlatformClassLoader@60db1c0e
        System.out.println(c1);

        // PlatformClassLoader 的父加载器是 Bootstrap
        // 通过调试器观察 c1.parent,其类型为 jdk.internal.loader.ClassLoaders$BootClassLoader
        // 它是使用 C++ 来实现的,getParent() 方法返回 null
        ClassLoader c2 = c1.getParent();
        // null
        System.out.println(c2);
    }
}
  

最高一层的加载器 BootStrap 是通过 C/C++ 实现的,并不存在于 JVM 体系内。
第二、三层类加载器为 Java 语言实现,用户也可以自定义类加载器。

低层次的当前类加载器,不能覆盖更高层次类加载器已经加载的类。

可以在启动时添加 -XX:+TraceClassLoading 参数来观察加载了哪个 jar 包中的哪个类。此参数在解决类冲突时非常实用,毕竟不同的 JVM 环境对于加载类的顺序并非是一致的。

在什么情况下需要自定义类加载器?

  1. 隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
  2. 修改类加载方式:类的加载模型并非强制,除 Bootstrap 外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载。
  3. 扩展加载源:比如从数据库、网络进行加载。
  4. 防止源码泄露:Java 代码容易被编译和篡改,可以进行编译加密。那么类加载器也需要自定义,还原加密的字节码。

实现定义类加载器的步骤:继承 ClassLoader ,重写 findClass() 方法,调用 defineClass() 方法。

4.3 内存布局

*[JIT]: Just-in-Time,实时编译
*[JVM]: Java Virtual Machine, Java 虚拟机
*[JDK]: Java Development Kit, Java 开发工具包