一、什么是类的加载?
类的加载过程及其最终产品
JVM将class文件字节码文件加载到内存中, 并将这些静态数据转换成方法区中的运行时数据结构,在堆(并不一定在堆中,HotSpot在方法区中)中生成一个代表这个类的java.lang.Class 对象,作为方法区类数据的访问入口。
-
类加载子系统是 JVM 的一个具体运行时模块(规范中明确存在)
-
类的加载系统是对类加载机制整体的描述(更像是一个概念性说法,不是 JVM 内部的独立模块名称)
-
你可以理解为:
-
类加载子系统 = JVM 内部真正干活的组件
-
类的加载系统 = 类加载器体系 + 双亲委派 + 加载流程 这些机制的总称
-
二、类的生命周期
JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程。其中加载、检验、准备、初始化和卸载这个五个阶段的顺序是固定的,而解析则未必。为了支持动态绑定,解析这个过程可以发生在初始化阶段之后。

类的生命周期并非从"使用"开始,而是远早于此。它始于JVM"发现"需要一个类,并终结于这个类被从内存中彻底清理。整个过程由JVM的类加载子系统 主导,可分为七个明确的阶段:
加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载
其中,加载、验证、准备、解析、初始化 这五个阶段,构成了广义上的 "类加载"过程 ,是类加载子系统的核心职责。
第一阶段:加载
这是类加载过程的起点。此阶段的任务是找到类的二进制字节流(通常是 .class 文件),并将其静态存储结构转化为方法区中的运行时数据结构,最后在堆内存中生成一个代表该类的 java.lang.Class 对象。
关键动作与拓展知识:
-
"寻找"字节流 :JVM并未规定字节流必须来自一个
.class文件。这体现了Java强大的可扩展性。它可以来自:-
本地文件系统(最常见)。
-
网络(Applet技术的基础)。
-
运行时计算生成(动态代理技术,如
Proxy.newProxyInstance生成的类)。 -
由其他文件生成(如从JAR、WAR包中读取)。
-
从加密文件中读取并解密。
这种多样性正是通过 "类加载器" 实现的。
-
-
类加载器的层次与双亲委派模型:
-
启动类加载器 :最顶层,由C++实现,负责加载Java核心库(
JAVA_HOME/lib目录下的类)。它是所有类加载器的"始祖"。 -
平台类加载器 / 扩展类加载器 :负责加载一些扩展功能的库(
JAVA_HOME/lib/ext目录)。 -
应用程序类加载器:负责加载用户类路径(ClassPath)上的所有类。我们写的代码通常由它加载。
-
自定义类加载器:用户可以根据特殊需求(如热部署、代码加密、模块化隔离)自己实现类加载器。
双亲委派模型 是类加载的"工作流程":当一个类加载器收到加载请求时,它首先不会自己尝试加载,而是将这个请求向上委托给父加载器去完成。每一层都如此,只有当所有父加载器都无法完成(在自己的搜索范围内找不到该类)时,子加载器才会尝试自己加载。
-
核心好处:
-
安全性 :防止用户自定义一个恶意的核心类(如
java.lang.String)来替代JVM核心实现。 -
稳定性:确保了同一个类(由同一个加载器加载)在程序的各种模块中都是唯一的,避免了类的重复加载和冲突。
-
-
-
成果物 :在方法区(Metaspace)中创建了该类的结构模板(字段、方法、接口、常量池等信息),并在堆中创建了唯一的
Class对象,作为访问方法区数据的入口和镜像。
第二阶段:验证
这是一个至关重要的安全检查站。因为字节流来源广泛,甚至可能是被恶意篡改的,JVM必须确保其不会危害虚拟机的安全与稳定。
验证的四个层次:
-
文件格式验证 :验证字节流是否符合
.class文件格式规范(魔数、版本号、常量池类型等)。这是基于二进制字节流的验证。 -
元数据验证 :对方法区中的数据结构进行语义分析,确保符合Java语言规范(例如:这个类是否有父类?是否继承了不允许被继承的
final类?字段/方法是否与父类冲突?)。 -
字节码验证:最复杂的阶段。通过数据流和控制流分析,确定程序语义是合法、符合逻辑的(例如:方法体中的类型转换是否有效?跳转指令是否会跳到方法体以外的字节码?)。
-
符号引用验证 :发生在后续的解析阶段。当将符号引用转换为直接引用时,验证该引用是否能够被正确访问(例如:通过符号引用能否找到对应的类、字段、方法?访问权限是否允许?)。
拓展意义 :验证阶段确保了所有被加载的类在语言规范层面是"合法公民",为JVM的稳定运行筑起了第一道防火墙。大量的JVM参数(如 -Xverify:none)可以控制验证的严格程度,但生产环境绝不建议关闭。
第三阶段:准备
此阶段正式为类的静态变量分配内存 (在方法区中),并设置这些变量的默认初始值,而非程序代码中显式赋予的值。
核心要点与拓展:
-
分配的内存是"类变量"(static变量),不包括实例变量(实例变量将在对象实例化时随对象一起分配在堆中)。
-
设置的是"零值":
-
int->0 -
long->0L -
boolean->false -
引用类型 ->
null
-
-
特殊情况:常量(final static) :如果静态字段被
final修饰(即常量),且在编译期就能确定其值,那么在准备阶段就会被直接初始化为程序代码中指定的值。这是因为常量在编译后就被明确,不需要变动。
此阶段可以看作是为类的静态数据"分配宿舍并铺好空床铺"的过程。
第四阶段:解析
这是将符号引用替换为直接引用的过程。
-
符号引用 :一组用来描述所引用目标的符号,可以是任何形式的字面量,与虚拟机内存布局无关。在
.class文件的常量池中,类、方法、字段的引用都以符号引用的形式存在(例如java/lang/Object)。 -
直接引用:可以直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。它与虚拟机实现的内存布局直接相关。同一个符号引用,在不同虚拟机实例上翻译出来的直接引用通常不同。
解析的目标 :主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这七类符号引用进行解析。
拓展知识 :解析并不一定在准备之后、初始化之前完成。JVM规范允许在"首次使用"某个符号引用时才去解析它,这被称为 "晚期绑定"或"动态链接"。这为Java的动态扩展能力(如反射)提供了基础。相对的,在类加载阶段完成解析称为"早期绑定"。
第五阶段:初始化
这是类加载过程的最后一步,也是真正开始执行用户编写的Java程序代码(字节码)的阶段 。
在此阶段,JVM会执行类的 <clinit>() 方法(由编译器自动收集类中所有类变量的赋值动作 和静态代码块中的语句合并而成)。
关键特性与拓展:
-
触发时机:"主动使用"一个类时才会初始化。JVM规范严格规定了六种"主动使用"场景,包括:
-
创建类的实例(
new)。 -
访问或赋值类的静态字段(
getstatic,putstatic),但被final修饰的常量除外(已在准备阶段解决)。 -
调用类的静态方法(
invokestatic)。 -
使用反射(
Class.forName("..."))强制加载类。 -
初始化一个类的子类(会触发其父类的初始化)。
-
被指定为JVM启动时的主类(包含
main方法的类)。
-
-
线程安全 :
<clinit>()方法由JVM保证其在多线程环境下的正确加锁和同步。这意味着类的初始化在全局范围内是线程安全的。如果一个类正在被初始化,其他线程必须等待。 -
父类优先 :
<clinit>()方法执行顺序上,保证父类的初始化先于子类。这也意味着父类的静态代码块和静态变量赋值优先于子类。 -
非必需 :如果一个类没有静态代码块,也没有对静态变量的赋值操作,编译器可以不为这个类生成
<clinit>()方法。
初始化标志着类加载过程的完成。此时,类的所有静态变量都已按照开发者的意图被正确赋值,静态代码块也已执行完毕,类已经完全"就绪",可以被用来创建对象、调用方法了。
后续阶段:使用与卸载
-
使用 :在初始化完成后,类就进入了漫长的使用期。程序可以通过
Class对象创建实例、访问方法、读写字段。 -
卸载:当满足以下所有苛刻条件时,类可以被卸载,并从方法区(Metaspace)中回收:
-
该类的所有实例都已被垃圾回收。
-
加载该类的
ClassLoader实例已被垃圾回收。 -
该类对应的
java.lang.Class对象没有任何地方被引用,无法通过任何方式(包括反射)访问到该类的方法。在由系统类加载器加载的常规应用中,类的卸载几乎很少发生。但在频繁创建和销毁自定义类加载器的场景(如应用服务器、OSGi框架、热部署)下,类的卸载是管理元空间内存、防止内存溢出的关键机制。
-
三、类加载器
1、核心设计哲学:解耦与开放
类加载子系统最精妙的设计之一,便是将"通过一个类的全限定名来获取描述此类的二进制字节流 "这个关键动作,从JVM内部剥离 出来,交给外部的、独立的"类加载器"去完成。
我们可以这样理解:
-
JVM核心引擎:它只负责定义"类"在内存中应该是什么样子(运行时数据结构),以及如何验证、准备、初始化它。它制定了一套严格的"产品标准"(Class文件格式和生命周期)。
-
类加载器 :它则是一个独立的"供应商 "或"资源获取模块"。它的唯一核心职责,就是按照JVM给出的"类名"(全限定名),去各种各样的"仓库"里,找到符合要求的"原材料"(二进制字节流),然后交给JVM核心引擎去加工。
这种设计的巨大优势:
-
技术解耦:JVM不必关心字节流从何而来。无论来自本地硬盘、网络远端、内存数据库、加密文件,还是运行时动态生成,对于JVM引擎来说,它收到的都是一个待处理的、统一的字节流。这极大地简化了JVM核心的设计。
-
极致的可扩展性 :这是最革命性的好处。因为获取字节流的动作被抽象成了一个接口(
ClassLoader类),这意味着任何开发者都可以通过继承和重写这个接口的关键方法,来实现自己的"类获取逻辑"。-
你可以实现一个从网络服务器下载类的加载器(历史上Applet的基础)。
-
你可以实现一个对加密的.class文件进行解密的加载器,保护知识产权。
-
你可以实现一个在内存中动态生成类字节码的加载器(如CGLib、动态代理技术)。
-
你可以实现一个从非标准文件(如脚本文件)编译并生成字节码的加载器。
-
结论 :只要最终能提供符合格式的二进制字节流,类可以来源于任何地方、以任何形式存在。Java平台的动态性、热部署、模块化隔离等高级特性,全部根植于此。
-
-
沙箱安全与隔离的基础:不同的类加载器实例,构成了不同的"命名空间"。即使是同一个全限定名的类,被不同的类加载器加载,在JVM看来也是两个完全不同的、互不兼容的类。这为应用程序服务器(如Tomcat)实现多个Web应用(可能使用同一库的不同版本)的隔离提供了技术基础。
2、系统的基石:三层核心类加载器
为了保证Java程序最基本的运行秩序,JVM自身提供了三个不可或缺的类加载器,它们不是平行关系,而是有严格的父子层级关系。这种层级关系,是"双亲委派模型"得以运转的物理结构。
第一层:启动类加载器
-
身份与实现 :这是整个类加载器体系的始祖与根基 。它不是一个Java类,而是由JVM底层的C/C++代码实现 ,并内嵌在JVM内核之中。因此,在Java代码中你无法直接引用到它(获取其对象引用时通常为
null)。 -
神圣职责 :它负责加载最最核心的Java运行时库。这些库位于JRE安装目录下的
lib目录中(例如rt.jar,charsets.jar等),是Java世界得以存在的基石(如java.lang.*,java.util.*,java.io.*等核心包)。 -
特权地位:由它加载的类,享有最高的信任级别。它们是虚拟机自身的一部分,其完整性决定了整个JVM的稳定性。
第二层:平台类加载器(旧称:扩展类加载器)
-
身份与实现 :这是一个由Java语言编写的、实实在在的类(
sun.misc.Launcher$ExtClassLoader)。它是"启动类加载器"的直接子加载器。 -
核心职责 :它负责加载Java的扩展功能库 。这些库位于JRE安装目录下的
lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中。这些库用于对Java核心平台进行功能扩展。 -
承上启下:它继承自启动类加载器,又是应用程序类加载器的父加载器。在JDK 9模块化系统之后,其角色和范围有所调整,但"加载非核心标准扩展"的基本定位依然存在。
第三层:应用程序类加载器(或称:系统类加载器)
-
身份与实现 :同样是一个Java类(
sun.misc.Launcher$AppClassLoader)。它是"平台类加载器"的直接子加载器。 -
核心职责 :这是与开发者日常编程关系最密切 的类加载器。它负责加载 "用户类路径" 下的所有类库。所谓"用户类路径",就是通过
-classpath或-cp参数指定的路径,或者是环境变量CLASSPATH所包含的路径。我们自己编写的所有Java代码(.class文件),以及项目依赖的第三方Jar包,几乎都是由这个类加载器加载的。 -
默认加载器 :在代码中,通过
ClassLoader.getSystemClassLoader()方法获取到的,通常就是这个应用程序类加载器。它也是程序中默认的上下文类加载器。
四、双亲委派

协同工作机制:双亲委派模型
这三层类加载器并非各自为战,它们通过一个被称为 "双亲委派模型" 的强制性的工作流程来协同工作。这个模型是保证Java程序稳定性的关键。
工作流程(通俗版):
当一个类加载器(例如应用程序类加载器)收到一个类加载请求时(比如要加载 com.example.MyClass),它绝对不会立即尝试自己去加载。它会做以下事情:
-
向上委托(询问父辈) :它首先将这个请求逐级向上传递 给自己的父加载器(请求链:
应用程序类加载器 -> 平台类加载器 -> 启动类加载器)。 -
顶层检查:启动类加载器首先在自己的"势力范围"(核心库目录)里查找。如果找到了,就由它加载,流程结束。
-
逐级下行:如果启动类加载器没找到(说明不是核心类),则请求会返回到平台类加载器,让它在其"势力范围"(扩展库目录)里查找。如果找到了,就由它加载,流程结束。
-
自己动手:如果平台类加载器也没找到(说明不是核心类也不是标准扩展类),请求最终才会回到最初发起请求的应用程序类加载器这里。此时,它才会真正执行自己的加载逻辑,去用户类路径下寻找并加载这个类。
双亲委派的核心价值:
-
绝对的安全性 :防止核心API被恶意篡改。假设有个不法分子自己写了一个
java.lang.String类放在用户类路径下,由于双亲委派机制,加载请求会优先交给顶层的启动类加载器,而启动类加载器会在核心rt.jar中找到官方的String类并加载。用户自定义的String类永远不会被加载,从而杜绝了通过伪造核心类破坏系统的可能。 -
避免类的重复加载 :确保了同一个类在JVM中只有一份定义。只要父加载器加载了某个类,子加载器就没有必要、也无法再次加载它。这保证了像
java.lang.Object这样的基础类,在全虚拟机范围内有且仅有一个Class对象。 -
清晰的职责边界:每一层加载器都有明确的加载范围,形成了清晰的层次化治理结构,使得类库的依赖和管理井然有序。
