类加载机制深度解析
一、类加载的触发时机
首先,Java中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加载。同时,Java还支持动态加载类,即在运行时通过程序来加载类,这为Java程序带来了更大的灵活性。
类会在以下情况下被JVM加载:
-
主动引用(必定触发类加载)
-
创建类实例:
new Object(),如果没有被加载过就会触发类加载
-
访问类的静态变量或方法(非final)如果该类还没有被加载,则会触发类的加载。
-
使用反射调用:
Class.forName("com.example.Test")
如果该类还没有被加载,则会触发类的加载。 -
初始化子类时,父类会先被加载
-
JVM启动时指定的主类(包含main方法的类)
-
当JVM启动时,会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等。
-
被动引用(不会触发类加载)
-
访问static final常量(编译期优化)
-
通过数组定义类:
TestClass[] arr = new TestClass[10]
-
获取Class对象:
TestClass.class
二、类加载过程的详细细节
更加详细的可以见我的另一篇博客:【2025最新面试常考Java八股】一个类的生命周期是怎么样的?-CSDN博客
1. 加载阶段(Loading)
-
二进制字节流获取:通过全限定名获取.class文件的二进制流
-
方法区存储:将类的结构信息(版本、字段、方法等)存入方法区
-
Class对象创建:在堆中生成对应的Class对象,作为方法区数据的访问入口
2. 验证阶段(Verification)
-
文件格式验证:检查魔数(0xCAFEBABE)、版本号等
-
元数据验证:检查语义是否正确(如是否有父类、是否final等)
-
字节码验证:通过数据流分析确保方法体合法
-
符号引用验证:检查引用的类、方法、字段是否存在
3. 准备阶段(Preparation)
-
静态变量内存分配:为类变量分配内存
-
初始值设置:
-
基本类型:默认零值(int=0,boolean=false等)
-
引用类型:null
-
例外:static final常量直接赋真实值
-
4. 解析阶段(Resolution)
将符号引用转换为直接引用:
-
类/接口解析:将类名转换为方法区的实际类引用
-
字段解析:解析字段所属的类和类型
-
方法解析:验证方法存在性及访问权限
-
接口方法解析:类似方法解析
5. 初始化阶段(Initialization)
-
执行clinit方法:按顺序执行静态变量赋值和静态代码块
-
线程安全:JVM会加锁确保只执行一次
-
父类优先:保证父类的初始化先于子类
三、双亲委派机制详解
1. 类加载器层次结构(大概就是这样,自下而上)
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
Bootstrap ClassLoader(启动类加载器) ↑ Extension ClassLoader(扩展类加载器) ↑ Application ClassLoader(应用类加载器) ↑ 自定义ClassLoader
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现却非常简单,实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中,代码简单,逻辑清晰易懂:先检查类是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
双亲委派模型主要是由ClassLoader#loadClass实现的,我们只需要自定义类加载器,并且重写其中的loadClass方法,即可破坏双亲委派模型。
那为什么需要双亲委派模型?有什么好处
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。
相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
3. 核心代码实现
对于Java自带的类加载器来说,当一个类被加载的时候,需要用到类加载器将类从外部加载到Jvm的内存当中,如下代码所示:
是线程安全的,如上面的源码所示,在loadClass方法中,是被synchronized加了锁的
protected Class<?> loadClass(String name, boolean resolve) { //加锁保证类加载的线程安全 synchronized (getClassLoadingLock(name)) { // 1. 检查是否已加载 Class<?> c = findLoadedClass(name); if (c == null) { try { // 2. 父加载器不为null则委派给父加载器 if (parent != null) { c = parent.loadClass(name, false); // 对于bootstap类加载器来说,他是没有父加载器的,所以用bootstrap加载该类 } else { // 3. 父加载器为null则委派给Bootstrap c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
} // 如果还没加载到,说明此类的二进制文件还没有定位到,需要使用自己的类加载器 if (c == null) { // 4. 父加载器都无法加载时,自己加载 c = findClass(name); } } return c; } }
对于自己加载的情况下,调用的findClass方法
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
. . .
}
}
再结合ClassLoader#findClass是protect的且为空实现,所以我们可以发现,findClass的作用就是给子类去加载其他二进制文件使用的,同时,还应该调用ClassLoader#defineClass去将二进制文件加载为Class类
通过上面的代码,我们可以发现如下几个步骤:
1缓存思想,如果该类已经被加载过,则不加载
2使用双亲委派模型,对该类进行加载
3如果通过CLASSPATH找不到该类的定义,则会通过findClass让子类自定义的去获取类定义的二进制文件
4然后通过defineClass将二进制文件加载为类
loadClass和findClass
findClass用于重写类加载逻辑、loadClass方法的逻辑里如果父类加载器加载失败则会调用自己的findClass方法完成加载,保证了双亲委派规则。
1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可
2、如果想打破双亲委派模型,那么就重写整个loadClass方法
4. 设计优势
-
安全防护:防止核心API被篡改(如自定义java.lang.Object类)
-
避免重复加载:保证类的唯一性
-
职责分明:每个加载器专注自己的加载范围
5. 打破双亲委派的场景
-
SPI机制:JDBC等服务接口由Bootstrap加载,实现类由线程上下文类加载器加载
-
OSGi:模块化热部署需求
-
Tomcat:Web应用隔离需求
四、实战案例分析
1. 类加载顺序示例
class Parent { static { System.out.println("Parent静态块"); } } class Child extends Parent { static { System.out.println("Child静态块"); } } // 输出顺序: // Parent静态块 // Child静态块
2. 双亲委派验证
public class Test { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); // null(Bootstrap加载) System.out.println(Test.class.getClassLoader()); // AppClassLoader } }
3.数组是怎么被加载的?
首先我们要明白,数组也是一种类,而不是基本数据类型。所以数组也和其他正常的类一样,需要被加载。
我认为数组类的类对象不是由类加载器创建的,而是根据 Java 运行时的要求自动创建的。 Class.getClassLoader()返回的数组类的类加载器与其元素类型的类加载器相同;如果元素类型是原始类型,则数组类没有类加载器。
破坏双亲委派的例子(会单独出一篇博客写这个)
tomcat是一个典型的例子,大部分人应该也有所了解。一个web容器可能需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的,如果采用默认的类加载机制,那么就会无法加载多个相同的类。
Tomcat 为了实现隔离性,所以并没有完全遵守双亲委派的原则:
五、常见问题排查
-
ClassNotFoundException
-
检查classpath配置
-
确认类文件是否存在
-
检查类加载器是否正确
-
NoClassDefFoundError
-
类加载成功但初始化失败
-
检查静态代码块是否有异常
-
LinkageError
-
版本冲突导致
-
检查依赖库版本一致性