JVM - 类加载机制

642 阅读4分钟

灵笼_002_白月魁_2160X909.png

前言

我们编写的 Java 代码,通过 javac 编译为 .class 文件,称为 字节码 。字节码由 JVM 加载,运行时解释器将字节码解析为机器码执行。即时编译器针对热点代码,将对应的字节码编译为机器码,达到更高的执行效率。

JVM 加载 class 字节码的过程称为 类加载。类加载的最终产物是 Class 对象,Class 对象封装了类在方法区内的数据结构,并向程序员提供了访问方法区内数据结构的接口。

下图显示了 Java 类的生命周期。 image.png

其中,类加载包含了 加载 (Loading)链接 (Linking)初始化 (Initialization) 三个阶段。链接阶段又包含了 验证,准备,解析 三个阶段。

类加载过程

第一阶段 加载阶段

在加载阶段,总共要做三件事:

  • 通过 全类名 获取类的 二进制字节流 (包含 class 文件)
  • 将获取的二进制字节流所代表的静态存储结构转换为 方法区 的运行时数据结构
  • 中生成一个 java.lang.Class 对象,作为该类的访问入口

需要注意的是,获取的二进制字节流除了可以是 class 文件,还包括其他的来源:

  • zip 包中读取,包括 JAR,WAR
  • 网络获取
  • 运行时动态生成,比如 动态代理技术
  • 其他文件生成,包括 加密文件

第二阶段 链接阶段

链接阶段包含了 验证准备解析 三个阶段。

验证

验证的目的是 保证 Class 文件的字节流中的信息符合虚拟机的要求,确保被加载类的正确性。

此过程包含了四种验证:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

但是,真正发生在验证阶段的只有 元数据字节码 验证。文件格式验证发生在加载阶段,符号引用验证发生在解析阶段。

准备

准备阶段为 static 变量 分配内存空间并设置 默认值

由于 final 变量在 编译时就会分配内存,所以如果是 static final 修饰的变量,在准备阶段就会直接赋值。

解析

解析阶段将符号引用替换为直接引用。解析可以发生在初始化之前(静态解析),也可以发生在初始化之后(动态解析,比如多态,后期绑定)。

第三阶段 初始化

初始化阶段就是执行类的构造器方法(不是构造方法) clinit() 的过程。该方法无需定义,是 javac 编译器自动收集类中 所有类变量的赋值动作静态代码块中 的语句合并而来的。

clinit() 中的指令是按照 代码的语句顺序 依次排列执行,虚拟机必须保证一个类的 clinit() 方法在多线程下同步加锁。

一个例子

public class ClinitTest {
    private static int num1 = 1;
    static {
        num1 = 2;
        System.out.println("static : " + num1); // ok
        num2 = 20;
//        System.out.println("static : " + num2); // error:illegal forward reference
    }

    private static int num2 = 10;
    public static void main(String[] args) {
        System.out.println("num1 = " + num1);   // 2
        System.out.println("num2 = " + num2);   // 10
    }
}

以上代码的输出结果应该是:

static : 2
num1 = 2
num2 = 10

在准备阶段,num1num2 都会分配内存,并赋值为 0

到了初始化阶段,按照代码的顺序:

  • 先将 num1 初始化为 1
  • 然后执行静态代码块中的内容
    • num1 初始化为 2
    • 然后输出 static : 2
    • 接着将 num2 初始化为 20
  • 然后再将 num2 初始化为 10
  • 最后执行 main 方法

由于 num2 内存再准备阶段就分配了,所以即使定义在静态代码块后面,也可以对它赋值,但是要注意,不能在静态代码块中调用 num2

后期绑定

在解析阶段,我们提到了如果是实现 后期绑定,那么解析会发生在初始化之后。那么什么是后期绑定呢?

后期绑定(又称动态绑定)是指在 运行时 才确定真正所调用的类型,这是实现多态的基础。

与之相对 前期绑定(静态绑定)是指在编译器就确定了真正的类型。

我们知道要发生多态,必须要重写父类的方法,所以 调用父类方法 以及 无法被重写的方法static,final,private方法和构造方法)都是前期绑定,其他的情况都是后期绑定。

动态绑定只针对方法,属性都是静态绑定。

// 父类
class Pet{
    String name = "pet";

    public String info(){
        return this.name;
    }
}

// 子类
class Cat extends Pet{
    String name = "cat";

    @Override
    public String info() {
        return this.name;
    }
}

public class BondTest {
    public static void main(String[] args) {
        Pet pet = new Cat();
        System.out.println(pet.name); 
        System.out.println(pet.info()); 
    }
}

以上代码输出结果为:

pet
cat

这时因为属性都是静态绑定,而重写方法是动态绑定实现的。