目录
(1) 启动类加载器(Bootstrap ClassLoader)
(2) 扩展类加载器(Extension ClassLoader)
(3) 应用程序类加载器(Application ClassLoader)
一、类加载
(一)类加载概述
类加载是 Java 虚拟机(JVM)将类的字节码文件(.class)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被 JVM 直接使用的 Java 类型的过程。
类加载的时机
-
创建类的实例
-
访问类的静态变量
-
调用类的静态方法
-
反射调用(Class.forName())
-
初始化子类时(父类需先初始化)
-
JVM 启动时被标记为启动类的类
(二)类加载过程详解
类加载过程分为五个阶段:加载 → 验证 → 准备 → 解析 → 初始化
1. 加载(Loading)
-
核心任务:查找并加载类的二进制数据
-
具体操作:
-
通过类的全限定名获取其二进制字节流
-
将字节流转化为方法区的运行时数据结构
-
在堆中创建对应的
java.lang.Class
对象,作为方法区数据的访问入口
-
示例:
// 当执行以下代码时会触发加载
Class<?> clazz = Class.forName("com.example.MyClass");
2. 验证(Verification)
-
目的:确保加载的类符合 JVM 规范,不会危害虚拟机安全
-
验证阶段:
-
文件格式验证(魔数、版本号等)
-
元数据验证(语义分析)
-
字节码验证(程序逻辑校验)
-
符号引用验证(常量池中的引用检查)
-
3. 准备(Preparation)
-
核心任务:为类变量(static 变量)分配内存并设置初始值
-
重要细节:
-
仅分配类变量,不包括实例变量
-
初始值为数据类型的零值(0, false, null 等)
-
若变量有 ConstantValue 属性(final static),则直接赋值
-
示例:
public static int value = 123;
// 准备阶段后 value = 0
// 初始化阶段后 value = 123
4. 解析(Resolution)
-
核心任务:将常量池中的符号引用替换为直接引用
-
解析类型:
-
类或接口解析
-
字段解析
-
类方法解析
-
接口方法解析
-
符号引用 vs 直接引用:
-
符号引用:用一组符号描述所引用的目标
-
直接引用:指向目标的指针、偏移量或句柄
5. 初始化(Initialization)
-
核心任务:执行类构造器
<clinit>()
方法 -
重要细节:
-
<clinit>()
由编译器自动收集类中的所有类变量的赋值动作和静态代码块生成 -
JVM 保证子类的
<clinit>()
执行前,父类的<clinit>()
已执行 -
多线程环境下会被正确加锁同步
-
示例:
public class InitializationExample {
static {
System.out.println("静态代码块执行");
}
public static int value = 123;
}
二、类加载器
(一)类加载器层次结构
(二) 类加载器分类
(1) 启动类加载器(Bootstrap ClassLoader)
-
C++ 实现,是 JVM 的一部分
-
负责加载 Java 核心库(
<JAVA_HOME>/lib
目录) -
不继承
java.lang.ClassLoader
-
获取时为
null
(2) 扩展类加载器(Extension ClassLoader)
-
sun.misc.Launcher$ExtClassLoader
实现 -
负责加载扩展库(
<JAVA_HOME>/lib/ext
目录) -
父加载器为启动类加载器
(3) 应用程序类加载器(Application ClassLoader)
-
sun.misc.Launcher$AppClassLoader
实现 -
负责加载用户类路径(ClassPath)上的类库
-
默认的类加载器
-
父加载器为扩展类加载器
(4) 自定义类加载器
-
继承
java.lang.ClassLoader
-
可实现热部署、模块隔离等特殊需求
(三)双亲委派模型
-
工作原则:
-
收到加载请求时,先委托给父加载器
-
父加载器无法完成时,自己尝试加载
-
-
工作流程:
-
优点:
-
避免重复加载
-
保证核心类库安全
-
确保类的一致性(如 java.lang.Object)
-
-
代码实现:
protected Class<?> loadClass(String name, boolean resolve) {
synchronized (getClassLoadingLock(name)) {
// 1. 检查类是否已加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 2. 委托父加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
// 3. 自己尝试加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
破坏双亲委派模型
-
场景:
-
SPI 服务加载(如 JDBC)
-
OSGi 模块化系统
-
热部署需求
-
-
解决方案:线程上下文类加载器
// 设置上下文类加载器
Thread.currentThread().setContextClassLoader(customClassLoader);
// 获取上下文类加载器
ClassLoader loader = Thread.currentThread().getContextClassLoader();
(四)自定义类加载器
1. 实现步骤
-
继承
java.lang.ClassLoader
-
重写
findClass()
方法 -
在
findClass()
中实现类加载逻辑 -
调用
defineClass()
将字节数组转为 Class 对象
2. 示例代码
public class CustomClassLoader extends ClassLoader {
private final 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 className) {
String path = classPath + File.separatorChar +
className.replace('.', File.separatorChar) + ".class";
try (InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
3. 使用场景
-
热部署:不重启应用更新类
-
模块隔离:不同模块使用不同版本库
-
代码加密:加载加密的类文件
-
从非标准位置加载类(网络、数据库等)
(五)类加载器重要方法
1. loadClass() vs findClass()
-
loadClass()
:实现双亲委派逻辑 -
findClass()
:自定义类加载的实际加载点
2. defineClass()
-
将字节数组转为 Class 对象
-
核心的类定义方法
-
通常由
findClass()
调用
3. resolveClass()
-
执行类的连接(解析阶段)
-
可选调用,可使类提前完成解析
(六)常见问题与解决方案
1. ClassNotFoundException
-
原因:类加载器找不到类定义
-
解决:检查类路径和类名拼写
2. NoClassDefFoundError
-
原因:编译时存在,运行时缺少类依赖
-
解决:确保所有依赖类在运行时可用
3. LinkageError
-
原因:类加载过程中出现不一致
-
解决:检查类版本冲突和类加载器隔离
4. 类加载器内存泄漏
-
原因:类加载器未释放导致加载的类无法卸载
-
解决:合理设计类加载器生命周期
(七)类卸载机制
-
条件:
-
类的所有实例已被回收
-
类的 Class 对象没有被引用
-
加载该类的 ClassLoader 已被回收
-
-
注意:由启动类加载器加载的类不会被卸载