Java虚拟机类加载机制
类加载机制是什么
Java虚拟机把描述类的字节码文件(.class)从外部存储介质加载到 JVM 内存,并对数据进行校验、转换解析和初始化,最终转换为可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。
与那些在编译时需要进行连接的语言不同,在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。
类加载的完整生命周期
一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

加载
加载(Loading)阶段是整个类加载(Class Loading)过程中的一个阶段,在加载阶段,Java虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合 JVM 规范,没有安全隐患。
- 文件格式验证
字节流是否符合Class文件格式的规范; - 元数据验证
对类的元数据信息进行语义校验,是否符合 Java 语法规范; - 字节码验证
对类的方法体(Class文件中的Code属性)进行校验分析; - 字节码验证
类的符号引用是否合法,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中,这时候类变量在方法区就完全是一种对逻辑概念的表述了。
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
- 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
- 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。
初始化
类的初始化阶段是类加载过程中最后一个执行的核心阶段,其本质是执行类构造器 <clinit>() 方法的过程。
<clinit>() 方法是 Javac 编译器根据类中静态代码逻辑自动拼接生成的特殊方法,并非程序员显式定义,核心特性如下:
- 内容构成:仅包含两类代码,且严格按代码书写顺序拼接:
静态变量(类变量)的显式赋值语句;
静态代码块(static {})中的执行逻辑。 - 生成规则:如果类中没有静态变量赋值和静态代码块,Javac 编译器不会为该类生成 () 方法,初始化阶段无任何执行逻辑;
- 调用规则:() 方法无参数、无返回值,仅由 JVM 在初始化阶段主动调用,程序员无法手动调用、重写或覆盖该方法。
- 线程安全:JVM 会保证
<clinit>()方法的执行是线程安全的,为<clinit>()方法的执行加锁,保证其线程安全且仅被执行一次。多个线程同时触发某个类的初始化时,只有一个线程能执行该类的<clinit>()方法,其他线程会阻塞等待,直至该方法执行完成,避免静态数据初始化混乱。
初始化的触发条件:主动引用
VM 严格规定仅当遇到以下 5 种主动引用场景 时,才会触发类的初始化(若类尚未初始化):
- 通过 new 关键字创建类的实例;
- 调用类的静态变量(非编译期可知的常量)或静态方法;
- 通过反射 API 主动调用类;
- 初始化某个子类时,若其父类尚未初始化,则先触发父类的初始化;
- 运行包含 main() 方法的主类(程序启动时,JVM 会优先初始化主类)。
不触发初始化的场景:被动引用
以下场景为被动引用,未真正使用类的核心静态数据,不会触发类的初始化:
- 引用类的编译期可知的静态常量
示例:System.out.println(Student.MAX_AGE);(MAX_AGE是static final int MAX_AGE = 100;)
编译期可知的常量会被 Javac 直接嵌入到调用类的字节码中,运行时调用类无需访问原类的字节码,因此不会触发原类的初始化(注:若 final 变量的值为运行时确定,如 static final int num = new Random().nextInt();,仍会触发初始化)。 - 子类引用父类的静态变量 / 静态方法
JVM 会直接定位到父类获取静态数据,仅触发父类的初始化,子类不会被初始化。 - 定义类的数组
仅创建了引用类型数组,数组元素为 null,未创建类的实例,也未访问类的静态数据,不会触发目标类的初始化。 - 调用类加载器的 loadClass () 方法加载类
loadClass() 方法仅执行类加载的 "加载" 阶段,不会触发后续的验证、准备、解析、初始化阶段(区别于 Class.forName(),后者默认触发初始化)。
初始化的执行顺序
初始化阶段的执行顺序遵循以核心规则,且 JVM 会严格保证规则的执行:
- 父类优先于子类:初始化子类前,必须先完成父类的初始化。
特殊规则(接口):初始化接口的实现类时,不会主动初始化父接口,仅当实现类直接使用父接口的静态变量时,才会触发父接口的初始化(接口无静态代码块,<clinit>()仅包含常量赋值)。 - 静态内容按书写顺序执行:类中的静态变量赋值语句、静态代码块,严格按照在源码中出现的先后顺序执行。
- 仅执行一次:一个类的
<clinit>()方法在 JVM 生命周期内仅执行一次;即使后续多次触发该类的主动引用,也不会重复执行初始化逻辑。
在我前面的这篇博客里演示了类的加载顺序代码 Java的static关键字
类加载器
类加载器是 Java 虚拟机(JVM)实现类加载阶段的核心组件,其核心职责是:根据类的全限定名(如java.lang.String)查找并加载对应的字节码文件(.class),将字节码二进制数据转换为 JVM 方法区中可识别的运行时数据结构,并在堆中生成对应类的java.lang.Class对象(作为访问该类数据的唯一入口)。类加载器不仅是连接 "外部 Class 文件" 与 "JVM 运行时数据" 的桥梁,还支撑了类隔离、类热部署、自定义字节码加载来源(如网络 / 数据库)等高级能力。
类加载器的本质分类(JVM 视角)
- 启动类加载器(Bootstrap ClassLoader)
由 C/C++ 语言编写,是 JVM 内核的一部分,并非 Java 类(打破 "类加载器也需要被加载" 的循环); - 用户级类加载器
所有非启动类加载器均由 Java 语言实现,独立于 JVM 内核,且全部继承自抽象类java.lang.ClassLoader;
类加载器的双亲委派模型
- 启动类加载器(Bootstrap Class Loader):
类加载器负责加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,那直接使用null代替即可。 - 扩展类加载器(Extension Class Loader):
- 它负责加载
<JAVA_HOME>\lib\ext目录中,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。JDK的开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载Class文件。 - 应用程序类加载器(Application Class Loader):
负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器,也被称为系统类加载器(System ClassLoader)。

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型
双亲委派模型是类加载器的核心协作规则(JDK 默认实现,非 JVM 强制规范),其核心逻辑是 "先委托父加载器,自身最后加载",具体执行流程:
- 当任意类加载器收到类加载请求时,首先不自行加载,而是将请求委派给其父加载器;
- 父加载器重复上述委托行为,直至请求传递到最顶层的启动类加载器;
- 启动类加载器检查自身加载路径,若能找到目标类则直接加载并返回Class对象;若无法加载,将请求回传给下一级加载器;
- 各级子加载器依次检查自身路径,若父加载器无法加载则自行尝试加载;若所有加载器均无法加载,抛出ClassNotFoundException。
双亲委派模型的优势
-
保障 JVM 核心类的安全与完整性,从源头防止核心 API 被篡改
核心原理:双亲委派模型强制所有类加载请求先委托给父加载器,使得自定义的 "核心类重名类" 无法绕过启动类加载器的优先级被加载,杜绝核心 API 被恶意替换或篡改的可能。
典型示例:若开发者编写一个java.lang.String类并放入应用 ClassPath 中,程序触发该类的加载请求时,请求会按 "应用程序加载器→扩展类加载器→启动类加载器" 的顺序委派;启动类加载器会优先加载 rt.jar 中的原生String类,自定义String类永远不会被加载。此外,JVM 还对java.lang包做了额外安全校验,即使尝试通过自定义类加载器直接加载该包下的类,也会抛出SecurityException。
-
保证类的唯一性,维护 Java 类型体系的基础稳定
核心原理:JVM 判定两个类是否相同的必要条件是类的全限定名完全一致以及加载该类的类加载器相同。双亲委派模型确保 "全限定名相同的类仅能被最顶层能加载它的类加载器加载",从而保证同一个类在 JVM 中只有一个版本,避免类型冲突。
典型示例:java.lang.Object是所有 Java 类的父类,无论应用中哪个类加载器发起加载Object的请求,最终都会委派给启动类加载器加载。这使得所有类的父类Object是同一个类,保证了 Java 核心特性的正常运行。
-
构建类的优先级层次,规范 JVM 类加载秩序
核心原理:双亲委派模型为类加载器建立了 "启动类加载器 → 扩展类加载器 → 应用程序加载器 → 自定义加载器" 的优先级层次,类的加载范围也对应 "核心类库→扩展类库→应用类→自定义类" 的边界。这种层次化设计让类加载的职责清晰可追溯,避免不同加载器重复加载同一类,也避免加载范围混乱。
典型示例:Spring 框架的org.springframework.context.ApplicationContext属于应用级类,由应用程序加载器加载;JDK 扩展类com.sun.nio.zipfs.ZipFileSystem由扩展类加载器加载;JDK 核心类java.util.ArrayList由启动类加载器加载。不同层级的类各司其职,不会出现 "核心类被应用加载器重复加载""扩展类被自定义加载器错误加载" 的情况。
双亲委派模型的破坏
双亲委派模型是 JDK 为类加载器设计的推荐实现规范,而非 JVM 层面的强制性约束。尽管 JVM 内置的核心类加载器均遵循该模型,但在 Java 发展过程中,因历史兼容、模型自身缺陷、业务动态性需求等原因,出现过三次大规模打破双亲委派模型的场景。这些场景并非对模型的否定,而是针对特定问题的适配性调整,核心仍围绕类加载的安全性与灵活性平衡展开。
第一次破坏:JDK 1.2 前的历史兼容(被动打破)
背景与核心原因:
双亲委派模型在 JDK 1.2 中才被正式引入,但 java.lang.ClassLoader 抽象类早在 Java 1.0 时期就已存在。早期开发者自定义类加载器时,普遍直接重写loadClass()方法(该方法后续被 JDK 1.2 纳入双亲委派的核心逻辑),以此实现自定义类加载规则 ------ 这导致 JDK 1.2 引入双亲委派模型时,无法通过技术手段强制禁止重写loadClass(),只能为兼容已有代码做出妥协。
解决方案:新增findClass()方法引导规范实现
JDK 1.2 的设计者对ClassLoader做了关键调整,既兼容历史代码,又引导新代码遵循双亲委派:
保留loadClass()方法并内置双亲委派逻辑:该方法会先委托父加载器加载类,仅当父加载器失败时,才调用子类的findClass()方法;
新增protected方法findClass():将自定义类加载的具体逻辑抽离到该方法中,引导开发者仅重写findClass(),而非直接修改loadClass()的委派逻辑;
核心分工:loadClass() 负责 "委派规则",findClass() 负责 "具体加载实现",defineClass() 负责 "字节码转 Class 对象",三者分工明确,成为自定义类加载器的标准范式。
关键意义
这次调整本质是 "被动兼容下的规范引导":既不破坏已有自定义类加载器的运行,又让新开发的类加载器默认遵循双亲委派,避免因重写loadClass()直接破坏委派逻辑。
第二次破坏:模型自身缺陷引发的 "逆向委派"
背景与核心矛盾:
双亲委派模型的核心是 "子加载器向上委托父加载器" 的单向逻辑,但它无法解决 "父加载器需要加载子加载器管辖范围内的类" 的场景,即 "基础类调用用户自定义类" 的矛盾:
基础类(如 JNDI、JDBC 核心类)由启动类加载器加载(存于 rt.jar),属于 "父加载器管辖范围";这些基础类需要调用用户实现的 SPI(服务提供者接口)类(如 MySQL JDBC 驱动、自定义 JNDI 实现),这类类存于应用 ClassPath,仅能由应用程序类加载器(子加载器)加载;启动类加载器无法向下访问子加载器的类,双亲委派的 "单向委派" 逻辑在此完全失效。
以 MySQL JDBC 驱动为例,直观体现该矛盾:
java.sql.DriverManager(启动类加载器加载)是 JDBC 核心类,负责管理和加载驱动;
com.mysql.cj.jdbc.Driver(应用程序加载器加载)是用户依赖的驱动实现类,需被DriverManager识别并注册;
若严格遵循双亲委派,DriverManager(启动类加载器)无法加载com.mysql.cj.jdbc.Driver(应用程序加载器),导致驱动无法注册,JDBC 连接无法建立。
解决方案 1:线程上下文类加载器(TCCL)
JDK 引入线程上下文类加载器(Thread Context ClassLoader) 打破 "单向委派" 限制,是解决 SPI 加载的核心方案:
定义:每个 Java 线程绑定一个类加载器,可通过Thread.setContextClassLoader(ClassLoader)主动设置;若未设置,会继承父线程的类加载器;全局默认值为应用程序类加载器。
核心原理:基础类通过Thread.currentThread().getContextClassLoader()获取线程绑定的应用程序加载器,逆向委托该子加载器加载 SPI 实现类,本质是 "父加载器请求子加载器完成加载",突破双亲委派的单向层级限制。
适用场景:JNDI、JDBC、JCE、JAXB、JBI 等所有涉及 SPI 的加载场景均采用此方案。
解决方案 2:ServiceLoader类(JDK 6 + 优化)
JDK 6 引入java.util.ServiceLoader类,替代 SPI 加载的硬编码实现,让逆向委派更规范:
配置规则:SPI 实现类需在META-INF/services/[SPI接口全限定名]文件中声明(如META-INF/services/java.sql.Driver中写入com.mysql.cj.jdbc.Driver);
加载逻辑:ServiceLoader通过线程上下文类加载器读取上述配置文件,自动加载并实例化 SPI 实现类,结合责任链模式管理多 SPI 实现,消除了硬编码判断的弊端。
第三次破坏:动态性需求驱动的 "主动打破"
背景与核心需求:
双亲委派模型下,一个类加载器加载的类一旦进入 JVM,除非加载该类的类加载器被完全回收,否则类本身无法被卸载(JVM 类卸载的核心条件)。但生产系统对 "动态性" 的需求(代码热替换、模块热部署)要求 "不重启应用即可更新类代码",这就需要打破 "类仅被最顶层加载器加载一次" 的规则。
典型场景:Tomcat 的类加载器设计
Tomcat 作为经典的 Java Web 容器,为实现 "不同 Web 应用类隔离" 和 "模块热部署",完全重构类加载器体系,主动打破双亲委派:
核心规则:WebApp 类加载器收到加载请求时,先自行加载应用内的类(优先加载WEB-INF/classes和WEB-INF/lib下的类),仅当自身无法加载时,才委托父加载器(Common)------ 与双亲委派 "先委托、后自己" 的逻辑完全相反;
热部署原理:更新 Web 应用时,直接销毁该应用对应的 WebApp 类加载器(及其加载的所有类),再创建新的 WebApp 类加载器加载更新后的字节码,实现不重启 Tomcat 的热部署。
延伸:代码热替换(Hot Swap)
核心思路:通过自定义类加载器加载待更新的类,当代码更新后,销毁旧加载器并创建新加载器加载新字节码 ------ 由于新类由新加载器加载,JVM 会将其视为 "不同类",从而实现代码的热替换;
局限:仅能替换方法体逻辑,无法修改类结构,否则会因类结构不一致引发LinkageError。
补充:JDK 9 + 模块化对委派模型的调整
JDK 9 引入模块化系统(Module System)后,传统三层类加载器体系(Bootstrap → Extension → Application)调整为 "Bootstrap → Platform → System",双亲委派模型也随之适配:
核心变化:类加载器不再严格遵循 "全量向上委托",而是基于模块依赖的 "按需委派"------ 优先加载自身模块路径下的类,仅当模块未导出相关包时,才委托父加载器;
本质:模块化的隔离需求让委派逻辑更灵活,但并未抛弃双亲委派的核心思想(核心类仍由 Bootstrap 加载,保证安全性;类唯一性仍由 "加载器 + 全限定名" 保证)。