深入理解Java虚拟机-类加载机制
类加载过程
加载(Loading)
- 将类的字节码文件(.class文件)加载到内存,并生成对应的Class对象(存储在方法区)
- 查找字节码:通过类的全限定名(如java.lang.String)定位字节码文件
- 读取字节码:将字节码转换为二进制流
- 生成Class对象:在方法区中创建类的Class对象(后续反射操作的基础)Class<?> clazz = Class.forName("com.example.MyClass"); // 触发加载阶段
验证(Verification)
- 确保字节码符合JVM规范,防止恶意代码或错误字节码危害JVM安全
- 文件格式验证:检查魔数(0xCAFEBABE)和版本号是否合法,验证常量池中的常量类型是否有效
- 元数据验证:检查类是否有父类(除Object外)、是否继承final类、字段/方法是否与父类冲突若一个类尝试继承final类(如String),验证阶段会抛出java.lang.VerifyError
- 字节码验证:确保方法体的字节码不会导致JVM崩溃(如操作数栈溢出、跳转到不存在的指令)
- 符号引用验证:检查符号引用(如类名、方法名)是否合法,确保解析阶段能正确绑定
准备(Preparation)
- 为类的静态变量(类变量)分配内存,并设置初始值(零值)
解析(Resolution)
- 将常量池中的符号引用转换为直接引用(内存地址或偏移量)
- 符号引用:一组符号描述目标(如
java/lang/Object
) - 直接引用:指向目标的指针、偏移量或句柄
- 类/接口解析:将类名符号引用转换为对应的
Class
对象 - 字段解析:将字段名转换为内存中的偏移量
- 方法解析:将方法名转换为方法入口地址
- 解析阶段可能在初始化之后触发(如动态绑定)
初始化(Initialization)
- 执行类的初始化代码(<clinit>()方法),为静态变量赋真实值,执行静态代码块
- 触发条件创建类的实例(new)访问类的静态变量(非常量)或静态方法反射调用(如Class.forName())初始化子类时,父类需先初始化JVM启动时指定的主类(包含main()的类)
- 初始化顺序:父类的<clinit>()先于子类执行,静态变量赋值和静态代码块按代码顺序执行
- 若多个线程同时初始化一个类,JVM会保证同步(仅一个线程执行<clinit>())
类加载总结
加载 |
文件 |
对象 | 查找字节码,生成内存结构 |
验证 | 字节码 | 合法字节码 | 检查格式、元数据、字节码逻辑 |
准备 | 静态变量符号引用 | 静态变量内存分配(零值) | 分配内存,设置默认值 |
解析 | 符号引用 | 直接引用 | 绑定类、字段、方法的内存地址 |
初始化 | 静态变量和代码块 | 类完全可用 | 执行
,赋真实值,执行静态代码块 |
常见问题与解决方案
- 类加载失败:原因:类路径错误、字节码损坏、版本不兼容解决:检查-classpath配置,确认类文件完整性
- 静态代码块死锁:原因:多线程初始化时,静态代码块内同步操作导致死锁解决:避免在静态代码块中使用复杂同步逻辑
- 类重复加载:原因:不同类加载器加载同一类解决:遵循双亲委派模型,避免自定义类加载器破坏机制
类加载器
类加载器的核心作用
- 加载字节码:从文件系统、网络、JAR包等来源读取字节码
- 类隔离:通过不同类加载器实现类的命名空间隔离(如Tomcat中的Web应用)
- 动态加载:支持运行时加载类(如插件化、热部署)
- 安全性控制:防止恶意代码替换核心类(如
java.lang.String
)
启动类加载(Bootstrap ClassLoader)
- 职责:加载JVM核心类库(jre/lib目录下的rt.jar、resources.jar等)Java 9+ 加载java.base(java.lang/java.util/java.io)、java.datatransfer、java.instrument
- 实现:由C/C++编写,是JVM的一部分,无Java类实例
- 访问限制:无法在Java代码中直接引用(getClassLoader()返回null)
扩展类加载器(Extension ClassLoader)/平台类加载器(Platform ClassLoader, Java 9+)
- 职责:加载扩展类库(jre/lib/ext目录下的JAR包)Java 9+ 加载java.sql、java.xml、java.logging、java.management
- 实现:Java类sun.misc.Launcher$ExtClassLoader
- 父加载器:启动类加载器
应用程序类加载器(Application ClassLoader)
- 职责:加载用户类路径(
-classpath
或CLASSPATH
环境变量)下的类 - 实现:Java类
sun.misc.Launcher$AppClassLoader
- 父加载器:扩展类加载器
- 默认类加载器:
ClassLoader.getSystemClassLoader()
返回此加载器
双亲委派模型(Parent Delegation Model)
- 委派父加载器:优先让父加载器尝试加载
- 父加载器失败:若父加载器无法加载,自己尝试加载。
- 最终失败:若所有加载器无法加载,抛出ClassNotFoundException
- 双亲委派的优势避免类重复加载:父加载器加载的类,子加载器不会重复加载保护核心类库:防止用户自定义类覆盖核心类(如自定义java.lang.Object
自定义类加载器
- 继承ClassLoader:重写findClass()方法
- 加载字节码:从自定义路径(如网络、加密文件)读取字节码
- 定义类:调用defineClass()生成Class对象
- 使用场景热替换:动态加载修改后的类(如调试环境)加密类加载:加载加密的字节码文件模块隔离:不同模块使用独立类加载器(如Tomcat的Web应用)
Tomcat的类加载器
- 层级结构:Common ClassLoader:加载Tomcat和Web应用共享的类WebApp ClassLoader:每个Web应用独立,加载WEB-INF/classes和WEB-INF/libJSP ClassLoader:动态加载JSP编译后的类,支持热替换
- 隔离机制:不同Web应用的类加载器相互隔离,避免类冲突
Spring的动态代理
- 场景:为接口生成代理类。
- 类加载器:使用
AppClassLoader
加载代理类,或通过Thread.currentThread().getContextClassLoader()
获取
OSGi模块化
- 机制:每个Bundle(模块)有自己的类加载器,按需动态加载依赖
- 优势:支持模块热插拔和版本共存
打破双亲委派
线程上下文类加载器(Thread Context ClassLoader)
- 背景:在SPI机制中,核心接口由启动类加载器加载,但实现类需由应用类加载器加载
- 通过
Thread.currentThread().setContextClassLoader()
设置线程上下文类加载器 - 在SPI代码中,使用
Thread.currentThread().getContextClassLoader()
加载实现类
自定义类加载器
- 背景:需要动态加载类或实现模块化隔离
- 继承
ClassLoader
,重写loadClass()
或findClass()
方法,直接加载类而不委派父加载器
OSGi模块化
- 背景:每个模块(Bundle)需要独立的类加载器,支持模块热插拔和版本共存
- 每个Bundle有自己的类加载器,按需加载依赖模块
- 通过
Import-Package
和Export-Package
声明模块间的依赖关系
打破双亲委派的应用场景
- SPI机制:JDBC、JNDI、JAXP等服务的实现类需由应用类加载器加载,使用线程上下文类加载器加载实现类
- 热部署:在开发或调试环境中,动态替换已加载的类,自定义类加载器直接加载新版本的类,旧版本类由GC回收
- OSGI模块化与插件化:不同模块或插件需要独立的类加载器,避免类冲突,每个模块或插件使用独立的类加载器