一、 引言:一个考试生的困惑
在学习JVM时,"类加载过程" 和**"双亲委派机制"**这两个概念总是让我又爱又恨。它们听起来很底层、很复杂,但面试官又特别喜欢问。
想象一下,你写的HelloWorld.java文件,是怎么变成计算机能执行的指令的?这个过程,类加载器就是背后的"搬运工"和"质检员",而双亲委派就是他们必须遵守的"工作流程"。
二、 类加载过程:一个类的"诞生"三部曲
一个类从.class文件到被JVM使用,需要经历三个核心阶段:加载 (Loading) 、链接 (Linking) 和 初始化 (Initialization) 。这个过程是懒惰的 (Lazy),也就是说,不到万不得已(不用到这个类),JVM是不会去加载它的。
1. 加载 (Loading) - "找户口,办身份证"
核心任务: 找到类的字节码文件(
.class),将其二进制数据读入内存的方法区 (Method Area) ,并在堆内存中创建一个代表该类的java.lang.Class对象。通俗比喻: 就像你去派出所上户口。派出所(类加载器)根据你的名字(全限定类名)找到你的档案(
.class文件),把档案信息录入系统(方法区),然后给你办一张身份证(Class对象)。以后在系统里,就用这张身份证来代表你。关键特性:
- 懒惰执行: 只有第一次真正用到这个类时(比如
new一个对象、访问静态变量等),才会触发加载。- 父类优先: 加载一个类时,如果它的父类还没加载,会先加载父类。这是继承关系的基础。
什么情况下会触发"加载"?
- 使用类的字面常量,如
System.class - 调用类加载器的
loadClass()方法 - (注意:很多资料会把"初始化"的触发条件也归为加载的触发条件,因为初始化前必然已完成加载和链接。)
2. 链接 (Linking) - "资格审查与关系绑定"
链接阶段又细分为三个小步骤:
① 验证 (Verification) - "检查档案真伪"
确保被加载的字节码是合法、安全的,符合JVM规范。比如检查魔数、版本号、字节码指令是否合法。防止有人篡改
.class文件危害JVM。② 准备 (Preparation) - "分配宿舍,铺好床铺"
为类的静态变量 (static变量) 在方法区分配内存,并设置数据类型的默认零值 (如int是0,boolean是false,引用是null)。**注意:**这里还不是赋值,赋值在初始化阶段。
③ 解析 (Resolution) - "把通讯录昵称换成手机号"
将常量池内的符号引用 (Symbolic References) 替换为直接引用 (Direct References)。符号引用就是一些字面量描述(如"java/lang/Object"),直接引用就是指向目标在内存中具体位置的指针或句柄。这个过程可能发生在初始化之后(延迟解析)。
3. 初始化 (Initialization) - "正式入住,开始工作"
核心任务: 执行类的静态代码块 (static {}) 和 为所有静态变量赋予程序中定义的初始值(不是默认零值)。
**通俗比喻:**户口和身份证都办好了,宿舍也分配了。现在你要正式入住,把行李(初始值)搬进宿舍,并执行入住仪式(静态代码块)。
关键特性: 初始化也是懒惰执行的,并且JVM保证一个类在多线程环境下只被初始化一次。
什么情况下会触发"初始化"?(非常高频的面试题!)
- 主类: 包含
main(String[])方法的类,在程序启动时初始化。 - 首次主动使用: 首次通过
new关键字创建实例、调用类的静态方法、访问类的非final静态字段。 - **子类初始化:**初始化一个类时,如果其父类还未初始化,会先触发父类的初始化。
- 反射调用: 使用
Class.forName("类名")或Class.forName("类名", true, classLoader)(第二个参数为true时)。 - **其他:**当初始化一个类时,如果发现其接口有默认方法(Java 8+)且该接口的实现类被初始化,则该接口要先被初始化。
三、 双亲委派机制 (Parent Delegation Model)

理解了类怎么加载,我们再来看看**"谁"来负责加载**,以及他们之间的"工作流程"。
1. 是什么?------ "孩子有事,先找家长"
双亲委派机制是Java类加载器(ClassLoader)工作时遵循的一种模型。它的工作原则是:
"当一个类加载器收到加载类的请求时,它首先不会自己去尝试加载,而是把这个请求++委派++给父类加载器去完成,直至最顶层才结束委派。每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader)。只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。"
Java中三类主要的类加载器(从父到子):
- 启动类加载器 (Bootstrap ClassLoader): 最顶层,由C++实现,负责加载
<JAVA_HOME>/lib下的核心类库(如rt.jar)。它是所有加载器的"老祖宗",没有父加载器。 - 扩展类加载器 (Extension ClassLoader): 由Java实现,负责加载
<JAVA_HOME>/lib/ext目录或系统变量指定路径下的类库。 - 应用程序类加载器 (Application ClassLoader / System ClassLoader): 由Java实现,负责加载用户类路径(ClassPath)上的类库。我们平时写的代码,默认就是由它加载的。
除了这三个,用户还可以自定义类加载器。
2. 实际场景描述 ------ "找一本《Java核心编程》"
假设你(应用程序类加载器)想找一本叫java.lang.String的书(类)。
- **委派:**你不会直接去你家书柜(ClassPath)找,而是先问你爸(扩展类加载器):"爸,你有这本书吗?"
- **再委派:**你爸也不会直接去他的书柜(ext目录)找,而是去问你爷爷(启动类加载器):"爸,您有这本书吗?"
- 顶层处理: 你爷爷一看,
java.lang.String?这是咱家的传家宝(核心rt.jar里的类)啊,在我这!于是他把书给了你爸,你爸再传给你。你自己根本不用找。
另一个场景: 你想找一本你自己写的com.example.MyApp。
- 你问你爸,你爸问你爷爷。
- 你爷爷说:"我这没有(核心库没这个类)。" 你爸也说:"我这也没有(ext目录里也没有)。"
- 这时,你才回到自己的书柜(ClassPath)里找,并且找到了。
3. 为什么这样做?(双亲委派的好处)
① 保证核心类库的安全与唯一性(最重要的原因!)
如果没有双亲委派,你自己写一个java.lang.String类放在ClassPath下,应用程序类加载器就会直接加载它,从而覆盖掉核心库里的String类。这会导致整个Java体系混乱,安全防线崩塌。双亲委派保证了像java.lang.*这样的核心类,永远由最顶层的启动类加载器加载,从而保证了Java核心API的纯洁性和不可篡改性。
② 避免类的重复加载
当父加载器已经加载了一个类,子加载器就没有必要再加载一次。这保证了在JVM中,一个类在其唯一的类加载器命名空间下是唯一的。这既是性能优化,也避免了因类重复定义导致的冲突。
③ 实现了类的优先级和沙箱隔离
核心库 > 扩展库 > 用户程序的加载优先级自然形成。同时,不同层级的类加载器形成了天然的隔离,为后面实现OSGi、Tomcat等容器提供了基础。
4. 为什么要"双亲"委派?(历史与设计)
"双亲"这个词容易误解为"两个父母",其实它指的是父级链 (Parent Chain) 。这种设计是一种责任链模式的应用。
- **从设计模式看:**它将加载请求逐级向上传递,让每个加载器只关心自己职责范围内的类,符合"单一职责"原则。
- **从系统架构看:**它建立了一个清晰、稳定的类查找秩序,顶层加载基础且稳定的类,底层加载多变的应用类,使得JVM的类生态井然有序。
- **"双亲"的由来:**在早期的设计中,类加载器确实有明确的父加载器和母加载器(用于加载接口)的区分,但后来简化了。现在"Parent"主要指代父级委托链。
四、 总结与记忆口诀
作为一名考试生,我是这样记忆的:
类加载"懒"字当头,三步走:
一加载(找档案办证),二链接(验身份、分宿舍、换电话),三初始化(搬行李搞仪式)。
触发条件记五个:Main、New、静态访问、子类初始化、反射forName。
双亲委派"稳"字当先,原则是:
"儿子有事找爸爸,爸爸有事找爷爷,爷爷没有才自己干。"
核心目的就一个:防止"熊孩子"(用户代码)篡改"家规"(核心类库),保证天下(JVM)只有一个"皇上"(如java.lang.Object)。