什么是类加载?
简单来说,类加载就是把.class文件(编译后的字节码)从磁盘/网络加载到JVM内存里,并生成对应的Class对象的过程。
Java类加载的5个核心阶段
类加载不是一步到位的,而是分成了5个阶段:加载→验证→准备→解析→初始化,其中加载、验证、准备、初始化的顺序是固定的,解析可能在初始化之后(为了支持动态绑定)。
1. 加载(Loading)
这是类加载的第一步,举个例子:假如你要加载User类,JVM会先找到User.class文件,把它的信息存到方法区,然后在堆里生成一个Class对象,以后你要访问User类的信息,都通过这个Class对象。
2. 验证(Verification)
这一步是为了保证JVM的安全,防止恶意或不规范的.class文件搞破坏,主要验证四个方面:
- 文件格式验证:检查
.class文件的魔数(开头的CAFEBABE)、版本号(JDK版本兼容)等; - 元数据验证:检查类的继承关系(比如不能继承
final类)、方法重写是否合法等; - 字节码验证:检查字节码指令是否安全(比如不会跳转到非法指令、不会访问越界的数组);
- 符号引用验证:检查符号引用(比如类名、方法名)是否能找到对应的类/方法。
3. 准备(Preparation)
这一步是在方法区里为类的静态变量(static修饰的变量)分配内存,并设置初始默认值。
需要注意的是:不是代码里写的初始值,而是以下这些初始值:
4. 解析(Resolution)
这一步是把常量池里的符号引用(比如类名com.example.User、方法名getUser),转换成直接引用(比如内存地址、指针)。
解析的话,主要针对的是类、接口、字段、方法等符号引用。
举个通俗一点的例子:
符号引用就是一个“代号”,比如你用“张三”指代一个人,不管他在哪,“张三”这个代号都能用;
而直接引用就是“具体的地址”,比如张三现在在“XX小区XX号楼XX室”,你可以直接找到他。
5. 初始化(Initialization)
这是类加载的最后一步,也是真正执行程序员写的代码的阶段。
这一步只做一件事:执行类构造器<clinit>()方法。
<clinit>()方法是编译器自动生成的,它会把类里的静态变量赋值语句和静态代码块(static {})按顺序合并起来执行。
举个例子
publicclassUser{staticint num = 10; // 静态变量赋值static { // 静态代码块 num = 20; System.out.println("静态代码块执行"); }}
以上代码在初始化阶段,JVM会执行<clinit>()方法:
先把num设为10,再设为20,然后打印“静态代码块执行”。
什么时候会触发类的初始化?
有以下6种情况:
- 访问类的静态变量或静态方法(
User.num、User.getUser()); - 用
Class.forName("com.example.User")反射加载类; - JVM启动时,会先初始化包含
main()方法的主类; - JDK 7+的动态语言支持,
MethodHandle解析结果为REF_getStatic/REF_putStatic/REF_invokeStatic的句柄,且对应的类未初始化。
双亲委派机制(Parent Delegation Model)
什么是类加载器?
类加载器(ClassLoader)就是负责加载类的工具,JVM内置了三种类加载器:
- 启动类加载器(Bootstrap ClassLoader)
用C++写的,不是ClassLoader的子类,开发者无法直接获取它的引用。
负责加载JDK核心类库(比如java.lang.Object、java.lang.String),这些类在jre/lib/rt.jar里。
- 扩展类加载器(Extension ClassLoader)
是ClassLoader的子类,开发者可以获取它的引用,负责加载JDK扩展类库(比如jre/lib/ext目录下的类)。
- 应用程序类加载器(Application ClassLoader)
是ClassLoader的子类,也是默认的类加载器。负责加载用户自己写的类(比如com.example.User),也就是classpath下的类。
什么是双亲委派机制?
这里的“双亲”不是指“父母”,而是指“父类加载器”,是翻译的问题,理解成“父委派机制”更准确一点。
双亲委派机制的核心规则是:“向上委托,向下加载”
- 当一个类加载器收到加载类的请求时,它不会自己先加载,而是把请求委托给父类加载器去加载;
- 如果父类加载器加载不了(比如在它的加载范围内找不到这个类),才会向下交给子类加载器自己加载。
举个例子
比如说:加载java.lang.Object。
- 应用程序类加载器收到加载
Object的请求,它不自己加载,委托给扩展类加载器; - 启动类加载器在
rt.jar里找到了Object类,直接加载它; - 加载完成,返回
Class对象,不会再向下交给子类加载器。
为什么需要双亲委派机制?
双亲委派机制的目的是保证Java程序的安全性和稳定性,主要解决了两个问题:
如果没有双亲委派,用户自己写一个java.lang.Object类,应用程序类加载器就会加载它,导致JVM里有两个Object类,程序就乱了。
防止用户恶意篡改核心类库(比如写一个恶意的java.lang.String类),因为核心类库只会由启动类加载器加载,用户写的同名类不会被加载。
破坏双亲委派机制的情况
常见的破坏双亲委派机制的场景:
JDBC需要加载不同数据库的驱动(比如MySQL驱动),但驱动类是用户写的,启动类加载器加载不了,所以DriverManager用了线程上下文类加载器(Thread Context ClassLoader),打破了双亲委派,让启动类加载器能委托应用程序类加载器加载驱动。
Tomcat需要隔离不同Web应用的类(比如两个应用都用了不同版本的Spring),所以它有自己的类加载器结构,每个Web应用有独立的类加载器,优先加载自己的类,而不是向上委托。