参考:

  1. Java 类加载器(ClassLoader)通俗详解:从原理到实战一、类加载器:JVM 的 “快递分拣员” 类加载器是 - 掘金
  2. Site Unreachable

背景

Java 虚拟机的世界里,类加载器(ClassLoader)扮演着不可或缺的角色。简单来说,它就像是一个智能的 “搬运工”,负责将编译后的 Java 类字节码文件加载到 JVM 的内存中,并将其转换为可以被 JVM 执行的 Java 类对象。

当我们编写好 Java 代码,经过编译生成.class文件后,这些文件还只是静态的字节码。要让它们在 JVM 中 “活” 起来,成为可以被程序使用的类,就需要类加载器的介入。类加载器会根据类的全限定名(包括包名和类名),在指定的位置(如文件系统、网络、JAR 包等)查找对应的字节码文件,然后将其读取到内存中,并创建出对应的java.lang.Class对象。这个Class对象就像是类的 “模板”,JVM 通过它来实例化对象、调用方法等。

Java 类加载器的存在,为 Java 程序带来了极大的灵活性和可扩展性。它使得 Java 程序可以在运行时动态加载类,而不是在启动时就加载所有的类。这样,我们可以根据实际需求,在需要的时候才加载特定的类,从而提高程序的启动速度和运行效率。同时,类加载器的层次结构和委派机制,也保证了类的唯一性和安全性,避免了类的重复加载和恶意篡改。

加载流程

jdk/src/hotspot/share/classfile/verifier.cpp at master · openjdk/jdk · GitHub

加载(Loading)
这是类加载的第一步,类加载器会根据类的全限定名(包括包名和类名),在文件系统、网络、JAR 包等指定位置查找对应的类文件字节码。例如,如果我们有一个类com.example.HelloWorld,类加载器会在相应的路径下寻找HelloWorld.class文件。找到文件后,它会将字节码读取到内存中,并将其转换为方法区中的运行时数据结构,同时在堆内存中生成一个代表这个类的java.lang.Class对象。这个Class对象就像是类的 “身份证”,后续 JVM 对类的各种操作,都需要通过它来进行。

连接(Linking)
连接阶段是类加载过程中的关键环节,它又细分为三个子步骤:验证、准备和解析。

  • 验证(Verification):这一步主要是确保被加载的类的字节码符合 Java 虚拟机规范,保证其安全性和正确性。例如,验证字节码文件的格式是否正确,是否存在恶意代码等。如果字节码文件不符合规范,JVM 会抛出VerifyError异常,阻止类的进一步加载。
  • 准备(Preparation):在这个阶段,JVM 会为类的静态变量分配内存,并设置默认的初始值。比如,对于一个静态整型变量public static int num;,在准备阶段,它会被初始化为 0。需要注意的是,这里只是为静态变量分配内存并设置默认值,并不会执行静态变量的显式赋值操作。
  • 解析(Resolution):解析的任务是将类、接口、字段和方法的符号引用解析为直接引用。在 Java 类的字节码中,对其他类、方法和字段的引用通常是用符号引用来表示的。例如,一个类中调用了另一个类的方法,在字节码中会以符号引用的形式记录这个方法的名称和所在类的信息。而在解析阶段,JVM 会将这些符号引用转换为直接引用,也就是直接指向目标对象的指针、偏移量或句柄,这样在运行时就能快速定位和访问目标对象。

初始化(Initialization)
初始化是类加载的最后一步,也是真正执行类初始化代码的阶段。在这个阶段,JVM 会按照程序中定义的顺序,执行类的静态变量赋值语句和静态代码块。例如:

Java 类加载器

双亲委派模型

类加载器遵循 “双亲委派” 规则,就像员工遇到问题先问上级:

  1. 当一个类加载器收到加载请求时,先交给父加载器处理;
  2. 父加载器依次向上传递,直到顶层的 Bootstrap ClassLoader;
  3. 若父加载器无法加载,才由当前加载器尝试加载。

Clipboard_Screenshot_1764597074.png

优势

  • 安全保障:防止用户自定义类覆盖核心类。例如,即使自己写了java.lang.Object,Bootstrap 加载器会优先加载官方Object,避免混乱。
  • 避免重复加载:同一个类只需加载一次,节省内存。

源代码

java.lang.ClassLoader#loadClass(java.lang.String, boolean)

  1. 检查缓存 → 是否已加载?
  2. 委托父加载器 → 父加载器能加载吗?
  3. 启动类加载器 → 是最顶层吗?
  4. 自己加载 → 调用 findClass
  5. 可选解析 → resolveClass
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) {
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) {
// If still not found, then invoke findClass in order
// to find the class. long t1 = System.nanoTime();
c = findClass(name);

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

启动类加载器 Bootstrap ClassLoader

简介

启动类加载器是 Java 类加载器层级结构的最顶层,它是用本地代码(如 C++)实现的 ,主要负责加载 Java 的核心类库,这些核心类库位于%JAVA_HOME%/lib目录下,像rt.jar、resources.jar等。这些核心类库包含了 Java 运行时必不可少的基础类,比如java.lang.Object、java.lang.String等。启动类加载器是 Java 虚拟机自身的一部分,它在 JVM 启动时就被创建,并且开发者无法直接在 Java 代码中获取到它的引用 。

如果在 Java 代码中尝试获取启动类加载器,得到的结果将是null。例如,在下面的代码中:

public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader bootstrapClassLoader = String.class.getClassLoader();
System.out.println(bootstrapClassLoader); // 输出null
}
}

这是因为启动类加载器不是由 Java 代码实现的,它处于 Java 类加载体系的最底层,是整个类加载机制的基础。JVM 启动时,会执行一个原生入口函数(例如 HotSpot 中的 main 函数),加载并初始化 JVM。在这个过程中,引导类加载器被创建并用于加载核心类库。

加载路径

Bootstrap ClassLoader 默认从以下位置加载类文件(具体路径可能因 JVM 实现和版本略有不同):

  • {JAVA_HOME}/jre/lib/(JDK 8 及之前)
  • {JAVA_HOME}/lib/(JDK 9+ 模块化系统后路径有所变化)

主要加载的 JAR 文件及配置文件:

文件名 描述
rt.jar Runtime Jar,包含 Java SE 标准库的核心类(如 java.lang.*java.util.*java.io.* 等)。这是最核心的运行时类库。
charsets.jar 提供各种字符集(Charset)的实现,如 UTF-8、GBK、ISO-8859-1 等,用于字符串编码与解码。
jce.jar Java Cryptography Extension,提供加密、密钥管理、安全随机数生成等密码学功能。
jsse.jar Java Secure Socket Extension,支持 SSL/TLS 协议,用于安全网络通信(如 HTTPS)。
resources.jar (某些 JVM 版本中存在)包含本地化资源文件(如区域设置数据)。
management-agent.jar 提供 JMX(Java Management Extensions)管理代理功能。

启动流程

  • 加载 JVM 动态库:操作系统加载 JVM 的动态链接库(如 libjvm.so 或 jvm.dll)。
  • 初始化 JVM:调用原生函数(如 JNI_CreateJavaVM)创建 JVM 实例。
  • 创建引导类加载器:JVM 用原生代码实现一个类加载器,负责从 /lib 或 /jmods 中读取核心类文件。
  • 加载核心类:引导类加载器将核心类的字节码加载到 JVM 的内存中(如方法区)。

特点

  1. 无父加载器:Bootstrap ClassLoader 是类加载器层次结构的根,没有父加载器。
  2. 不可见性:在 Java 应用程序中无法通过 getClassLoader() 获取到它(返回 null)。
  3. 安全性:加载的类被视为“可信系统类”,拥有最高权限,不受安全管理器(SecurityManager)限制(除非显式配置)。
  4. 启动顺序最早:JVM 启动时首先初始化 Bootstrap ClassLoader,然后才加载 sun.misc.Launcher 并创建 Extension 和 Application ClassLoader。
  5. 平台相关:其实现依赖于操作系统和 JVM 厂商(如 Oracle HotSpot、OpenJ9 等),行为可能略有差异。

扩展类加载器 Extension ClassLoader

扩展类加载器由sun.misc.Launcher$ExtClassLoader类实现,它负责加载 Java 的扩展类库。这些扩展类库通常位于%JAVA_HOME%/lib/ext目录下,或者由系统属性java.ext.dirs指定的路径中。开发者可以直接在 Java 程序中使用扩展类加载器 。例如,当我们想要获取扩展类加载器时,可以通过以下代码实现:

public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader extensionClassLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println(extensionClassLoader); // 输出sun.misc.Launcher$ExtClassLoader@...
}
}

扩展类加载器的作用是为 Java 平台提供了一种扩展机制,允许开发者将一些自定义的类库添加到扩展目录中,从而实现对 Java 核心功能的扩展。例如,一些第三方的 Java 库,如 JDBC 驱动的扩展实现,就可以通过扩展类加载器来加载。

应用程序类加载器 Application ClassLoader

应用程序类加载器由sun.misc.Launcher$AppClassLoader类实现,它负责加载用户类路径(classpath)下的类库。在我们日常开发中,自己编写的 Java 类以及引入的第三方依赖库,通常都是由应用程序类加载器来加载的。它是程序默认的类加载器,开发者可以直接在代码中使用它。我们可以通过ClassLoader.getSystemClassLoader()方法来获取应用程序类加载器,示例代码如下:

public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader applicationClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(applicationClassLoader); // 输出sun.misc.Launcher$AppClassLoader@...
}
}

应用程序类加载器在 Java 开发中扮演着非常重要的角色,它使得我们能够方便地管理和加载项目中的各种类,是我们与类加载机制交互最频繁的类加载器之一。

自定义类加载器 Custom ClassLoader

自定义类加载器主要用于加载一些特殊来源的类,比如从网络、数据库中加载类,或者对类进行加密和解密等特殊处理。在一些框架中,为了实现模块的隔离和热部署功能,会使用自定义类加载器来加载模块相关的类。

自定义类加载器的实现通常需要重写findClass方法,在这个方法中实现自定义的类加载逻辑。以下是一个简单的自定义类加载器示例:

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

public class CustomClassLoader extends ClassLoader {
private String classPath;

public CustomClassLoader(String classPath) {
this.classPath = classPath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
return defineClass(name, classData, 0, classData.length);
}

private byte[] loadClassData(String name) {
String className = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
try (FileInputStream fis = new FileInputStream(className);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer))!= -1) {
bos.write(buffer, 0, length);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}

在这个示例中,CustomClassLoader从指定的路径中读取类文件的字节码,并通过defineClass方法将字节码转换为Class对象。通过这种方式,我们可以实现对类加载过程的自定义控制,满足各种特殊的需求。