第三章:类加载子系统
类加载时机
类加载发生在程序运行过程中的以下关键阶段:
类加载器类型及职责:
2. 准备(Preparation)
为静态变量分配内存并设置默认初始值:
初始化阶段(Initialization)
执行类构造器<clinit>()
方法,该方法由编译器自动生成:
class MyClass {
static int a = initA(); // 静态变量赋值
static {
System.out.println("Static block"); // 静态代码块
}
static int b = initB();
static int initA() { return 1; }
static int initB() { return 2; }
}
类加载子系统是Java虚拟机(JVM)的核心组件,负责将类和接口的二进制数据(.class文件)加载到内存中,并将其转换为JVM能够使用的运行时数据结构。它在JVM架构中处于承上启下的位置:
核心功能:
定位和加载:从各种来源(文件系统、网络、JAR包等)查找并读取字节码
链接:将加载的类合并到JVM运行时环境
初始化:执行类的初始化逻辑
显式创建实例:遇到
new
指令时提供访问入口:创建java.lang.Class对象作为访问类元数据的接口
new MyObject(); // 触发MyObject类加载
访问静态成员:执行
getstatic
/putstatic
/invokestatic
指令时int value = MyClass.STATIC_FIELD; // 触发MyClass加载
反射调用:通过反射API操作类时
Class.forName("com.example.MyClass"); // 显式触发加载
子类初始化:初始化子类时父类尚未加载
class Child extends Parent {} // 加载Child时先加载Parent
JVM启动时:预先加载核心类如java.lang.Object
接口默认方法:实现接口的类初始化时
interface MyInterface { default void method() {} } class Impl implements MyInterface {} // 加载Impl时加载MyInterface
二、类加载过程
加载阶段(Loading)
加载阶段由类加载器完成,主要任务包括:
查找字节码:通过全限定类名查找二进制数据
读取字节流:将字节码读入内存
创建Class对象:在堆中生成java.lang.Class实例
-
类加载器 实现 加载路径 职责范围 Bootstrap C++ $JAVA_HOME/lib 核心Java库(rt.jar) Extension Java $JAVA_HOME/lib/ext 扩展库 Application Java $CLASSPATH 应用程序类 Custom Java 自定义 特殊需求 - 类加载器层次关系:
链接阶段(Linking)
1. 验证(Verification)
确保.class文件符合JVM规范:
文件格式验证:魔数(0xCAFEBABE)、版本号等
元数据验证:语义检查(是否有父类、是否实现接口等)
字节码验证:数据流和控制流分析
符号引用验证:确保引用的类/字段/方法存在
2. 准备(Preparation)
为静态变量分配内存并设置默认初始值:
public static int value = 123; // 准备阶段:value = 0 // 初始化阶段:value = 123 public static final int CONST = 456; // 准备阶段:CONST = 456 (final常量直接赋值)
3. 解析(Resolution)
将符号引用转换为直接引用:
类/接口解析:将类名转换为Class对象引用
字段解析:确定字段在内存中的偏移量
方法解析:定位方法实际入口地址
接口方法解析:类似方法解析
初始化阶段(Initialization)
执行类构造器
<clinit>()
方法,该方法由编译器自动生成:class MyClass { static int a = initA(); // 静态变量赋值 static { System.out.println("Static block"); // 静态代码块 } static int b = initB(); static int initA() { return 1; } static int initB() { return 2; } }
编译器生成的
<clinit>
方法字节码:0: invokestatic #2 // Method initA:()I 3: putstatic #3 // Field a:I 6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 9: ldc #5 // String Static block 11: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 14: invokestatic #7 // Method initB:()I 17: putstatic #8 // Field b:I 20: return
初始化规则:
父类优先于子类初始化
接口初始化不触发父接口初始化
多线程环境下初始化操作会被正确加锁
三、类加载器
双亲委派模型
双亲委派模型是Java类加载的基础机制:
工作流程:
类加载请求首先委派给父加载器
父加载器递归向上委派
顶层Bootstrap加载器尝试加载
父加载器无法加载时,子加载器尝试加载
最终由发起请求的加载器完成加载或抛出ClassNotFoundException
优势:
安全性:防止核心API被篡改
一致性:保证类在JVM中的唯一性
高效性:避免重复加载
自定义类加载器
实现步骤:
继承java.lang.ClassLoader
重写findClass()方法
在findClass()中读取字节码
调用defineClass()定义类
示例:数据库类加载器
public class DatabaseClassLoader extends ClassLoader {
private final DataSource dataSource;
public DatabaseClassLoader(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 1. 从数据库读取字节码
byte[] bytes = loadClassBytesFromDB(name);
// 2. 定义类
return defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassBytesFromDB(String className) {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(
"SELECT bytecode FROM class_store WHERE class_name=?")) {
stmt.setString(1, className);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getBytes("bytecode");
}
}
} catch (SQLException e) {
throw new RuntimeException("Failed to load class from DB", e);
}
throw new ClassNotFoundException(className);
}
}
四、类加载机制的优化与注意事项
性能优化
根源:类加载器与加载的类相互引用
2. 内存泄漏
类加载顺序:
案例3:Klass与InstanceKlass结构体
在HotSpot JVM中,类元数据通过Klass体系存储:
示例:查看String类布局
并行加载:使用
ClassLoader.registerAsParallelCapable()
启用并行加载public class ParallelClassLoader extends URLClassLoader { static { registerAsParallelCapable(); } // ... }
缓存优化:合理使用缓存但避免内存泄漏
private final Map<String, Class<?>> cache = Collections.synchronizedMap(new WeakHashMap<>());
类索引优化:在大型应用中优化类查找算法常见问题与解决方案
1. 类冲突问题
现象:
java.lang.LinkageError
或ClassCastException
ClassLoader loader1 = new CustomClassLoader(); ClassLoader loader2 = new CustomClassLoader(); Class<?> class1 = loader1.loadClass("com.example.MyClass"); Class<?> class2 = loader2.loadClass("com.example.MyClass"); // 看似相同的类,实际不同 System.out.println(class1 == class2); // false
解决方案:
使用同一类加载器加载相关类
使用接口隔离技术(OSGi规范)
2. 内存泄漏
根源:类加载器与加载的类相互引用
public class LeakyClassLoader extends ClassLoader { private final Map<String, Class<?>> classes = new HashMap<>(); @Override protected Class<?> findClass(String name) { // 加载类 Class<?> clazz = ...; classes.put(name, clazz); // 强引用导致无法GC return clazz; } }
解决方案:
private final Map<String, WeakReference<Class<?>>> classes = new ConcurrentHashMap<>();
深度案例分析
案例1:Tomcat类加载器架构
Tomcat需要同时支持:
应用隔离:不同Web应用使用独立类空间
资源共享:公共库只需加载一次
核心设计:
Common:加载Tomcat核心和公共库
Catalina:加载Tomcat内部实现
Shared:加载Web应用共享库
WebApp:每个Web应用独立加载器
类加载顺序:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 1. 检查本地缓存 Class<?> clazz = findLoadedClass(name); if (clazz != null) return clazz; // 2. 避免WebApp覆盖核心类 if (name.startsWith("java.")) { return super.loadClass(name, resolve); } // 3. 尝试WebApp类加载器 try { clazz = findClass(name); if (clazz != null) return clazz; } catch (ClassNotFoundException ignore) {} // 4. 委托给Shared类加载器 return super.loadClass(name, resolve); } }
案例2:热部署实现
热部署关键是通过新建类加载器实现类更新:
public class HotDeployer implements Runnable { private final String className; private final File classFile; private volatile Class<?> loadedClass; public HotDeployer(String className, File classFile) { this.className = className; this.classFile = classFile; } public void run() { // 1. 创建新的类加载器 URLClassLoader newLoader = new URLClassLoader( new URL[]{classFile.getParentFile().toURI().toURL()}, getClass().getClassLoader() ); try { // 2. 加载新版本类 Class<?> newClass = newLoader.loadClass(className); // 3. 创建实例并替换旧引用 Object newInstance = newClass.getDeclaredConstructor().newInstance(); loadedClass = newClass; // 4. 旧类加载器将在GC时卸载 } catch (Exception e) { // 处理异常 } } public Class<?> getCurrentClass() { return loadedClass; } }
卸载条件:
类的所有实例已被GC
类的ClassLoader实例已被GC
类的java.lang.Class对象没有引用
案例3:Klass与InstanceKlass结构体
在HotSpot JVM中,类元数据通过Klass体系存储:
// hotspot/share/oops/klass.hpp class Klass : public Metadata { // 共享元数据 volatile jint _layout_helper; Symbol* _name; }; // hotspot/share/oops/instanceKlass.hpp class InstanceKlass: public Klass { // 类特定元数据 Array<Method*>* _methods; Array<Klass*>* _local_interfaces; Array<Klass*>* _transitive_interfaces; InstanceKlass* _array_klasses; InstanceKlass* _java_mirror; ClassLoaderData* _class_loader_data; };
内存布局:
使用HSDB查看类元数据:
启动HSDB:
jdk/bin/java -cp sa-jdi.jar sun.jvm.hotspot.HSDB
附加到目标JVM进程
查看类信息:
Class Browser:浏览已加载类
Inspector:查看对象内存布局
Universe:查看堆内存概况
Class: java.lang.String Superclass: java.lang.Object Loader: bootstrap Fields: - value: [C @offset 12 - hash: I @offset 16 - coder: B @offset 20 Methods: - hashCode()I - equals(Ljava/lang/Object;)Z - ...
总结
类加载时机:由JVM规范严格定义,主要发生在首次主动引用时
加载过程三阶段:
加载:定位字节码并创建Class对象
链接:验证、准备和解析
初始化:执行
<clinit>
方法
双亲委派模型:保障安全性和一致性的核心机制
自定义类加载器:实现热部署、模块化等高级特性的关键
性能优化:并行加载、缓存管理和类索引优化
常见问题:类冲突和内存泄漏需特别关注
生产实践:
Tomcat通过分层加载器实现应用隔离
热部署通过新建类加载器实现类更新
Klass体系是JVM类元数据的内部表示
下一篇将从内存的角度去讲解jvm底层的实现逻辑,感兴趣的可以收藏持续关注