【2025最新Java八股面试常问知识点】类什么时候会被加载?加载过程中的细节有哪些?什么是双亲委派机制

类加载机制深度解析

一、类加载的触发时机

首先,Java中的类加载其实是延迟加载的,除了一些基础的类以外,其他的类都是在需要使用类时才会进行加载。同时,Java还支持动态加载类,即在运行时通过程序来加载类,这为Java程序带来了更大的灵活性。

类会在以下情况下被JVM加载:

  1. 主动引用(必定触发类加载)

  • 创建类实例:new Object(),如果没有被加载过就会触发类加载

  • 访问类的静态变量或方法(非final)如果该类还没有被加载,则会触发类的加载。

  • 使用反射调用:Class.forName("com.example.Test")如果该类还没有被加载,则会触发类的加载。

  • 初始化子类时,父类会先被加载

  • JVM启动时指定的主类(包含main方法的类)

  • 当JVM启动时,会自动加载一些基础类,例如java.lang.Object类和java.lang.Class类等。

  1. 被动引用(不会触发类加载)

  • 访问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 为了实现隔离性,所以并没有完全遵守双亲委派的原则:

五、常见问题排查

  1. ClassNotFoundException

  • 检查classpath配置

  • 确认类文件是否存在

  • 检查类加载器是否正确

  1. NoClassDefFoundError

  • 类加载成功但初始化失败

  • 检查静态代码块是否有异常

  1. LinkageError

  • 版本冲突导致

  • 检查依赖库版本一致性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值