跳转至

Java 类加载机制

需要理解:类的加载不是对象加载,和实例无关,而在于类本身,所以后面的一系列 加载、链接、初始化 操作都不会涉及到成员变量

类 从被加载到 JVM 开始,到卸载出内存,整个生命周期分为 7 各阶段:

image-20241224181357714

除去加载和卸载,就是 类加载 过程。这五个阶段一般是顺序发生的,但是在动态绑定的情况下,解析阶段发生在初始化阶段之后。

类加载的时机:它既可以是饿汉式[eagerly load](只要有其它类引用了它就加载)加载类,也可以是懒加载[lazy load](等到类初始化发生的时候才加载),具体的实现取决于 JVM

类加载过程

1. 载入

JVM 将来自不同数据源(class 文件/ jar 包 / 网络)的字节码转换为二进制字节流加载到内存中,并生成一个代表该类的 Class 对象

java 中的每个类型(包括类、接口、数组、基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应

Class 对象中包含了类的元数据

2. 验证

JVM 在此阶段对于刚才的二进制字节流进行校验(检查 Magic Number 是否为 cafe babe ,方法的参数个数和类型等)

3. 准备

JVM 在此阶段对 类变量静态变量)分配内存,并初始化为默认值(如 0,0L,null,false 等)

  • 静态变量:初始化为默认值
  • 常量:包括 final 和 static final ,准备阶段会被直接赋值为初始化的值

4. 解析

常量池中的符号引用转换为直接引用

.Class 文件(字节码文件)的常量池中会存储:

  • 字面量
  • 符号引用

常量池中的符号引用转换为直接引用:比如 class A 有class B 类型的成员变量,注意到编译时并不知道 B 的 Class 对象存放在哪里,所以编译期确定的是 B 的符号引用,在载入后才能知道 B 的 Class 对象的位置

对于符号引用的理解:

编译阶段不知道类 / 类字段 / 类方法的具体内存位置,则使用一种符号来描述目标,这组符号存放在常量池中

5. 初始化

类的初始化阶段会将静态变量的值由默认值变为声明这个变量时所赋的值。

初始化的时机如下

  • 创建类的实例时。
  • 访问类的静态方法或静态字段时(除了 final 常量,它们在编译期就已经放入常量池)。
  • 使用 java.lang.reflect 包的方法对类进行反射调用时。
  • 初始化一个类的子类(首先会初始化父类)。
  • JVM 启动时,用户指定的主类(包含 main 方法的类)将被初始化

类加载器

类在 JVM 中的唯一性由其类加载器和这个类的 .Class 字节码文件共同决定。这主要体现在 Class 对象的 equals 方法

双亲委派模型

  • BootstrapClassLoader
  • ExtensionClassLoader
  • AppClassLoader

类加载器层次关系图

加载器在加载一个类时,首先委派给这个类的父类的加载器去加载这个类,只有当父类加载器无法加载这个类时,子加载器才会尝试自己去加载。

  1. 委派给父加载器:当一个类加载器接收到类加载的请求时,它首先不会尝试自己去加载这个类,而是将这个请求委派给它的父加载器。这个过程会递归向上进行,从启动类加载器(Bootstrap ClassLoader)开始,再到扩展类加载器(Extension ClassLoader),最后到系统类加载器(System ClassLoader)。
  2. 加载类:如果父加载器可以加载这个类,那么就使用父加载器的结果。如果父加载器无法加载这个类(它没有找到这个类),子加载器才会尝试自己去加载。
  3. 安全性和避免重复加载:这种机制可以确保不会重复加载类,并保护 Java 核心 API 的类不被恶意替换。

如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现两个不同的 Object 类。双亲委派模型可以保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoaderBootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。