Java 类加载机制

类的生命周期

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段:加载、验证、准备、解析、初始化、使用和卸载。其中,验证、准备和解析这三个阶段可以统称为链接。

e4d69576-bfa3-4124-8bce-e122ab2921ed-1748857549789-13

加载(Loading)

  1. 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,程序员可以使用Java代码拓展的不同的渠道。

    • 从本地磁盘上获取文件
    • 运行时通过动态代理生成,比如Spring框架
    • Applet技术通过网络获取字节码文件
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中,方法区中生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。

    image-1748857581373-18

  3. Java虚拟机同时会在堆上生成与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。

    4c9d80c9-9bb3-4177-9d05-aa1f405f2e14-1748857604903-22

链接(Linking)

链接阶段将加载的类准备好以供JVM使用,分为以下三个子阶段:

验证(Verification)

此阶段会对字节码进行校验,确保其符合 Java 虚拟机规范,不会危害虚拟机的安全。验证过程包括:

  • 文件格式验证:检查类文件的魔数(是否以0xCAFEBABE开头)、版本等基本结构。
  • 元数据验证:检查类的内部结构,如字段、方法的描述符。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证: 确保解析动作能正确执行。

准备(Preparation)

准备阶段主要为类的静态变量分配内存,并设置其初始值(默认值)。注意一下几点:

  • static 变量分配空间和赋值是两个步骤,分配空间准备阶段完成,赋值初始化阶段完成。
  • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等)。
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成

解析(Resolution)

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

初始化(Initialization)

初始化阶段是类加载过程的最后一步,主要任务是执行类构造器()方法,该方法由编译器自动生成,用于初始化类的静态变量和执行静态块。初始化阶段包括:

  • 执行静态变量的初始化赋值。
  • 执行静态代码块。

类的初始化的懒惰的

  1. 以下情况会初始化

    • main 方法所在的类,总会被首先初始化
    • 首次访问这个类的静态变量或静态方法时
    • 子类初始化,如果父类还没初始化,会引发
    • 子类访问父类的静态变量,只会触发父类的初始化
    • 反射(如Class.forName)
    • 创建类的实例,也就是new的方式
  2. 以下情况不会初始化

    • 访问类的 static final 静态常量(基本类型和字符串)
    • 类对象.class 不会触发初始化
    • 创建该类对象的数组
    • 类加载器的.loadClass方法
    • Class.forNamed的参数2为false时

对上述准则的验证(注释下逐个验证)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Load2 {
public static void main(String[] args) {
// 不能初始化,final在准备阶段就已经赋值了
System.out.println(E.a);
// 不能初始化,final在准备阶段就已经赋值了
System.out.println(E.b);
// 会导致 E 类初始化,因为 Integer 是包装类
System.out.println(E.c);
}
}

class E {
public static final int a = 10;
public static final String b = "hello";
public static final Integer c = 20;

static {
System.out.println("E cinit");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Load3 {
static {
//在main类前面的静态代码块会优先初始化
System.out.println("main init");
}

public static void main(String[] args) throws ClassNotFoundException {
// 1. 静态常量(基本类型和字符串)不会触发初始化
System.out.println(B.b);
// 2. 类对象.class 不会触发初始化
System.out.println(B.class);
// 3. 创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4. 不会初始化类 B,但会加载 B、A
ClassLoader cl = Thread.currentThread().getContextClassLoader();
cl.loadClass("cn.itcast.jvm.t3.B");
// 5. 不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("cn.itcast.jvm.t3.B", false, c2);

// 1. 首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2. 子类初始化,如果父类还没初始化,会引发父类的初始化
System.out.println(B.c);
// 3. 子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4. 会初始化类 B,并先初始化类 A
Class.forName("cn.itcast.jvm.t3.B");
}
}

class A {
static int a = 0;

static {
System.out.println("a init");
}
}

class B extends A {
final static double b = 5.0;
static boolean c = false;

static {
System.out.println("b init");
}
}

使用

类访问方法区内的数据结构的接口, 对象是Heap区的数据。

卸载

Java虚拟机将结束生命周期的几种情况:

  • 执行了System.exit()方法
  • 程序正常执行结束
  • 程序在执行过程中遇到了异常或错误而异常终止
  • 由于操作系统出现错误而导致Java虚拟机进程终止

类加载器

类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。

根据官方 API 文档的介绍:

  • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
  • 每个 Java 类都有一个引用指向加载它的 ClassLoader
  • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

简单来说,类加载器的主要作用就是动态加载 Java 类的字节码( .class 文件)到 JVM 中(在内存中生成一个代表该类的 Class 对象)。

加载规则

  1. 动态加载机制
    • 按需加载:Java 类在首次使用时才会被加载(如通过new实例化、调用静态方法 / 字段等)。
    • 运行时加载:类加载过程在程序运行期间完成,而非编译时。
  2. 类的唯一性
    • 类的唯一性由 类加载器 + 类的全限定名(如java.lang.String) 共同确定。不同类加载器加载的同名类被视为不同的类。

类加载器类型

名称 加载的类 说明
Bootstrap ClassLoader(启动类加载器) %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类 由 JVM 底层(C++)实现,Java 代码中无法直接引用。
Extension ClassLoader(拓展类加载器) JAVA_HOME/jre/lib/ext sun.misc.Launcher$ExtClassLoader实现。
Application ClassLoader(应用程序类加载器) 当前应用 classpath 下的所有 jar 包和类 sun.misc.Launcher$AppClassLoader实现,是ClassLoader类的默认加载器。
自定义类加载器 自定义 继承java.lang.ClassLoader并重写关键方法(如findClass())。

寻找类加载器

每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过 BootstrapClassLoader 加载的。

寻找类加载器例子如下:

1
2
3
4
5
6
7
8
 public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}

结果如下:

1
2
3
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1b6d3586
null

从上面的结果可以看出,并没有获取到ExtClassLoader的父Loader,原因是BootstrapLoader(引导类加载器)是由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

双亲委派模型

Java采用了双亲委派模型来组织类加载器的层次结构。具体来说,当一个类加载器接收到类加载请求时,它会首先将请求委派给父类加载器处理,只有在父类加载器无法完成加载时,子类加载器才会尝试自己加载。这种机制确保了Java核心类库的安全性和一致性,避免了类的重复加载和命名冲突。

双亲委派机制过程

  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载。
  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

loadClass源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
//首先,检查类是否已经加载
Class<?> c = findLoadedClass(name);
if (c == null) {
//说明该类没有被加载过
long t0 = System.nanoTime();
try {
//判断父类是否为空
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 捕获异常但不处理,表示父类加载失败
}

if (c == null) {
long t1 = System.nanoTime();
//如果仍未找到,则调用 findClass 以查找该类。
//用户可通过覆写该方法,来自定义类加载器
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

自定义类加载器

使用场景
  • 想加载非 classpath 随意路径中的类文件
  • 通过接口来使用实现,希望解耦时,常用在框架设计
  • 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于 tomcat 容器
步骤
  • 继承ClassLoader父类
  • 要遵从双亲委派机制,重写 findClass 方法
  • 不是重写loadClass方法,否则不会走双亲委派机制
  • 读取类文件的字节码
  • 调用父类的 defineClass 方法来加载类
  • 使用者调用该类加载器的 loadClass 方法

ClassLoader 类有两个关键的方法:

  • protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
  • protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。

学习文献