【Java SE】双亲委派模型

双亲委派模型

在Java中,我们都知道静态代码块在类加载时执行且只执行一次 ,并且父类的静态代码块先于子类的静态代码块执行 。这个现象的背后,实际上是Java虚拟机(JVM)的类加载机制 ,特别是双亲委派模型在起作用。本文将深入探讨这两者之间的关系。

类加载的生命周期

类的生命周期

在理解静态代码块执行时机之前,我们需要先了解一个Java类从被加载到被卸载的完整生命周期:

复制代码
加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载
       ↑_____________|      
         连接阶段           

其中,初始化阶段是执行静态代码块和静态变量赋值的时刻。

什么是初始化?

JVM规范中明确规定:在初始化阶段,会执行类构造器<clinit>()方法。这个方法由编译器自动收集:

  • 所有静态变量的初始化语句(声明时的赋值)
  • 所有静态代码块中的代码

这些内容按照在源代码中出现的顺序,被合并到<clinit>()方法中。

进入<clinit>()方法的类型

java 复制代码
public class CompleteStaticDemo {
    
    // ===== 类型1:声明时赋值 → 进入<clinit> =====
    static int var1 = 100;                // 会进入<clinit>
    static String var2 = "hello";         // 会进入<clinit>
    static final int VAR3 = 300;          // 编译时常量 → 不会进入<clinit>
    static final String VAR4 = "world";    // 编译时常量 → 不会进入<clinit>
    
    // ===== 类型2:声明时不赋值 → 不进入<clinit> =====
    static int var5;                       // 只是声明,没有赋值
    static Object var6;                     // 只是声明,没有赋值
    static final int VAR7;                  // 声明时不赋值
    
    // ===== 类型3:复杂的静态变量初始化 =====
    static int var8 = new Random().nextInt(100);  // 运行时初始化 → 会进入<clinit>
    static List<String> var9 = new ArrayList<>(); // 对象创建 → 会进入<clinit>
    
    // ===== 类型4:静态代码块中的赋值 → 进入<clinit> =====
    static {
        var5 = 500;           // 赋值操作进入<clinit>
        var6 = new Object();  // 对象创建进入<clinit>
        VAR7 = 700;           // final变量在静态代码块中赋值 → 进入<clinit>
        
        System.out.println("静态代码块执行");
    }
    
    // ===== 类型5:静态方法中的赋值 → 不进入<clinit> =====
    static void init() {
        var5 = 1000;  // 这是在方法中,不属于<clinit>
    }
    
    public static void main(String[] args) {
        System.out.println("=== 静态变量分类演示 ===");
        
        // 验证final编译时常量(这些值在编译时就确定了)
        System.out.println("VAR3 = " + VAR3);  // 直接编译进字节码,不需要<clinit>
        System.out.println("VAR4 = " + VAR4);  // 直接编译进字节码,不需要<clinit>
    }
}

双亲委派模型

什么是双亲委派模型?

双亲委派模型(Parents Delegation Model)是Java类加载器的工作机制,其核心思想是:

当一个类加载器收到类加载请求时,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。只有当父类加载器无法完成这个加载请求时,子加载器才会尝试自己去加载。

类加载器的层次结构

大概意思是:"有事找上级,上级解决不了自己再解决。"

复制代码
 Bootstrap ClassLoader(启动类加载器)
          ↑
 Extension ClassLoader(扩展类加载器)
          ↑
 Application ClassLoader(应用程序类加载器)
          ↑
   自定义类加载器

双亲委派的工作流程

这个方法定义在 java.lang.ClassLoader 类中,是 Java 类加载器的核心方法。可以在以下位置找到它:JDK 源码:java.base/java/lang/ClassLoader.java

java 复制代码
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 首先检查是否已经加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载器无法加载
            }

            if (c == null) {
                // 如果父类加载器没有找到,调用自己的 findClass
                long t1 = System.nanoTime();
                c = findClass(name);

                // 这是定义类加载器;记录统计信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派如何影响静态代码块执行⭐

类加载的递归过程

假设我们有这样的类结构:

java 复制代码
class Parent {
    static {
        System.out.println("Parent静态代码块执行");
    }
}

class Child extends Parent {
    static {
        System.out.println("Child静态代码块执行");
    }
}

当程序第一次使用Child类时,类加载过程如下:

步骤1:应用程序类加载器收到加载Child的请求

复制代码
应用程序类加载器收到加载 Child 的请求
    ↓
检查 Child 是否已加载 → 否
    ↓
委派给父加载器(扩展类加载器)

步骤2:请求向上委派直到启动类加载器

复制代码
扩展类加载器收到请求
    ↓
检查 Child 是否已加载 → 否
    ↓
委派给父加载器(启动类加载器)
    ↓
启动类加载器收到请求
    ↓
检查 Child 是否已加载 → 否
    ↓
启动类加载器尝试加载 Child → 无法加载(不在rt.jar中)
    ↓
向下返回控制权给扩展类加载器

步骤3:扩展类加载器尝试加载

复制代码
扩展类加载器尝试加载 Child → 无法加载(不在ext目录中)
    ↓
向下返回控制权给应用程序类加载器

步骤4:应用程序类加载器开始加载Child

复制代码
应用程序类加载器准备加载 Child
    ↓
【关键点】解析Child类的class文件
    ↓
发现 Child extends Parent
    ↓
【触发Parent类的加载】
    ├─→ 检查Parent是否已加载 → 否
    ├─→ 按照双亲委派模型加载Parent
    │    ├─→ 委派给扩展类加载器
    │    ├─→ 扩展类加载器委派给启动类加载器
    │    ├─→ 启动类加载器尝试加载Parent → 无法加载
    │    ├─→ 扩展类加载器尝试加载Parent → 无法加载
    │    └─→ 应用程序类加载器加载Parent
    │         ↓
    │    Parent类加载完成
    │         ↓
    │    【Parent类的连接和初始化会在后面进行】
    └─→ 返回Child类的加载
    ↓
继续加载Child类的其他部分(字段、方法等)
    ↓
Child类加载完成(此时还没有初始化)

步骤5:初始化阶段(当真正使用类时)

复制代码
程序第一次真正使用Child类(如new Child()或Child.静态变量)
    ↓
JVM发现Child类需要初始化
    ↓
【JVM规范规定:初始化一个类前必须先初始化其父类】
    ├─→ 检查Parent是否已初始化 → 否
    ├─→ 初始化Parent类
    │    ├─→ 执行Parent的<clinit>()方法
    │    └─→ 输出"Parent静态代码块"
    └─→ Parent初始化完成
    ↓
初始化Child类
    ├─→ 执行Child的<clinit>()方法
    └─→ 输出"Child静态代码块"

完整的代码验证

java 复制代码
public class ClassLoaderParentDemo {
    
    public static void main(String[] args) {
        System.out.println("=== 程序开始 ===");
        System.out.println("此时Parent和Child都还未加载");
        
        System.out.println("\n--- 第1步:首次使用Child类 ---");
        System.out.println("触发类加载和初始化");
        System.out.println("执行结果:");
        
        // 这行代码会触发类加载和初始化
        Child child = new Child();
        
        System.out.println("\n--- 第2步:验证加载器 ---");
        System.out.println("Child的类加载器: " + Child.class.getClassLoader());
        System.out.println("Parent的类加载器: " + Parent.class.getClassLoader());
        
        System.out.println("\n--- 第3步:再次使用Child类 ---");
        System.out.println("静态代码块不会再次执行");
        Child child2 = new Child();
    }
}

class Parent {
    static {
        System.out.println("【Parent静态代码块执行】");
    }
    
    public Parent() {
        System.out.println("【Parent构造方法执行】");
    }
}

class Child extends Parent {
    static {
        System.out.println("【Child静态代码块执行】");
    }
    
    public Child() {
        System.out.println("【Child构造方法执行】");
    }
}

执行结果:

复制代码
=== 程序开始 ===
此时Parent和Child都还未加载

--- 第1步:首次使用Child类 ---
触发类加载和初始化
执行结果:
【Parent静态代码块执行】
【Child静态代码块执行】
【Parent构造方法执行】
【Child构造方法执行】

--- 第2步:验证加载器 ---
Child的类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@xxxx
Parent的类加载器: jdk.internal.loader.ClassLoaders$AppClassLoader@xxxx

--- 第3步:再次使用Child类 ---
静态代码块不会再次执行
【Parent构造方法执行】
【Child构造方法执行】
  1. Parent类的加载时机 :在加载Child类的过程中被触发,而不是在初始化阶段
  2. 加载顺序:Parent先被加载,Child后被加载
  3. 初始化顺序:Parent先初始化,Child后初始化
  4. 类加载器:Parent和Child通常由同一个类加载器加载(除非特殊配置)

完整流程图

复制代码
时间轴↓   应用程序类加载器                 Child类                     Parent类
   ↓           ↓                            ↓                          ↓
   1    收到加载Child请求                   未加载                      未加载
   2    ↓(双亲委派)
   3    父加载器都无法加载
   4    ↓(自己加载)
   5    开始加载Child
   6    解析Child的class文件
   7    发现继承自Parent
   8    ↓ 触发Parent加载
   9    【开始加载Parent】
  10      按照双亲委派加载Parent
  11      Parent类加载完成 ←──────────────────┐
  12    ↓ 返回继续加载Child                    │
  13    继续加载Child的其他部分                 │
  14    Child类加载完成                        │
  15    ↓(等待初始化)                         │
  16    首次使用Child(new)                    │
  17    需要初始化Child                         │
  18    检查Parent是否初始化 → 否 ──────────────┘
  19    【初始化Parent】
  20    执行Parent静态代码块
  21    Parent初始化完成
  22    【初始化Child】
  23    执行Child静态代码块
  24    Child初始化完成
  25    执行构造方法

深入理解"只执行一次"

类加载的缓存机制

双亲委派模型的一个重要优点是:避免类的重复加载

java 复制代码
public class SingletonClassDemo {
    
    public static void main(String[] args) {
        // 第一次触发加载
        System.out.println("第一次访问MyClass");
        MyClass.testStatic();
        
        // 第二次访问
        System.out.println("\n第二次访问MyClass");
        MyClass.testStatic();
     
    }
}

class MyClass {
    static {
        System.out.println("MyClass静态代码块执行!");
    }
    
    static void testStatic() {
        System.out.println("静态方法调用");
    }
}

执行结果:

复制代码
第一次访问MyClass
MyClass静态代码块执行!
静态方法调用

第二次访问MyClass
静态方法调用

为什么只执行一次?

双亲委派模型通过以下机制保证了"只执行一次":

  1. 缓存机制:每个类加载器都有自己的命名空间,已加载的类会被缓存
  2. 委派机制:同一个类在JVM中只会被加载一次
  3. 初始化标志:每个类都有初始化状态标记,已初始化的类不会再次初始化
java 复制代码
// JVM内部的处理逻辑(概念示意)
class ClassKlass {
    enum InitState {
        NOT_INITIALIZED,    // 未初始化
        INITIALIZING,       // 正在初始化
        INITIALIZED         // 已初始化
    }
    
    private InitState state = InitState.NOT_INITIALIZED;
    private ClassLoader loader;
    private Class<?> superClass;
    
    void initialize() {
        // 同步锁,确保线程安全
        synchronized (this) {
            if (state == InitState.INITIALIZED) {
                return;  // 已初始化,直接返回
            }
            
            if (state == InitState.INITIALIZING) {
                // 递归初始化处理
                return;
            }
            
            state = InitState.INITIALIZING;
            
            // 先初始化父类
            if (superClass != null && !superClass.isInitialized()) {
                superClass.initialize();
            }
            
            // 执行<clinit>方法
            callClinit();
            
            state = InitState.INITIALIZED;
        }
    }
}

特殊情况与面试题

不会触发初始化的场景

并不是所有对类的访问都会触发静态代码块执行:

java 复制代码
public class NotTriggerInitDemo {
    
    public static void main(String[] args) {
        System.out.println("1. 访问静态常量(不会触发初始化)");
        System.out.println(ConstClass.VALUE);
        
        System.out.println("\n2. 通过数组定义引用(不会触发初始化)");
        InitClass[] array = new InitClass[10];
        
        System.out.println("\n3. 访问父类的静态成员(不会触发子类初始化)");
        System.out.println(SubClass.PARENT_VALUE);
        
        System.out.println("\n4. 真正触发初始化的场景");
        System.out.println(SubClass.SUB_VALUE);
    }
}

class ConstClass {
    static final int VALUE = 123;  // 编译时常量
    
    static {
        System.out.println("ConstClass初始化!");
    }
}

class InitClass {
    static {
        System.out.println("InitClass初始化!");
    }
}

class ParentClass {
    static int PARENT_VALUE = 456;
    
    static {
        System.out.println("ParentClass初始化!");
    }
}

class SubClass extends ParentClass {
    static int SUB_VALUE = 789;
    
    static {
        System.out.println("SubClass初始化!");
    }
}

多线程环境下的初始化

双亲委派模型结合类初始化锁机制,确保了多线程环境下静态代码块的线程安全:

java 复制代码
public class ThreadSafeInitDemo {
    
    public static void main(String[] args) {
        // 创建多个线程同时访问
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                LazyClass.getInstance();
            }, "线程-" + i).start();
        }
    }
}

class LazyClass {
    private static LazyClass instance;
    
    static {
        System.out.println(Thread.currentThread().getName() 
            + " 执行静态代码块");
        // 模拟耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        instance = new LazyClass();
    }
    
    static LazyClass getInstance() {
        return instance;
    }
}

执行结果(只会有一个线程执行静态代码块):

复制代码
线程-0 执行静态代码块
线程-1 获取到实例
线程-2 获取到实例
线程-3 获取到实例
线程-4 获取到实例

静态代码块和静态变量的顺序⭐

java 复制代码
public class OrderTrick {
    static int x = 10;
    
    static {
        System.out.println("静态代码块1: x = " + x);
        x = 20;
    }
    
    static int y = x + 5;  // y会是多少?
    
    static {
        System.out.println("静态代码块2: x = " + x + ", y = " + y);
        x = 30;
        y = 100;
    }
    
    public static void main(String[] args) {
        System.out.println("main: x = " + x + ", y = " + y);
    }
}
复制代码
静态代码块1: x = 10
静态代码块2: x = 20, y = 25
main: x = 30, y = 100
  • <clinit>()方法按源代码从上到下的顺序执行
  • 静态变量声明时的赋值和静态代码块,按照出现顺序合并,后面的赋值会覆盖前面的值

静态代码块与构造方法执行顺序⭐

java 复制代码
public class ClassLoaderParentDemo {
    public static void main(String[] args) {
        Child child = new Child();
        System.out.println("==================");
        Child child2 = new Child();

        // 验证类加载器是否是同一个
        System.out.println("\n=== 验证类加载器 ===");

        // 获取两个对象的类加载器
        ClassLoader loader1 = child.getClass().getClassLoader();
        ClassLoader loader2 = child2.getClass().getClassLoader();

        System.out.println("child的类加载器: " + loader1);
        System.out.println("child2的类加载器: " + loader2);
        System.out.println("两个类加载器是否同一个实例: " + (loader1 == loader2));

        // 打印更详细的信息
        System.out.println("\n=== 详细比较 ===");
        System.out.println("loader1的hashCode: " + System.identityHashCode(loader1));
        System.out.println("loader2的hashCode: " + System.identityHashCode(loader2));
        System.out.println("loader1的类: " + loader1.getClass().getName());
        System.out.println("loader2的类: " + loader2.getClass().getName());
    }
}

class Parent {
    static {
        System.out.println("【Parent静态代码块执行】");
    }

    public Parent() {
        System.out.println("【Parent构造方法执行】");
    }
}

class Child extends Parent {
    static {
        System.out.println("【Child静态代码块执行】");
    }

    public Child() {
        System.out.println("【Child构造方法执行】");
    }
}
复制代码
【Parent静态代码块执行】
【Child静态代码块执行】
【Parent构造方法执行】
【Child构造方法执行】
==================
【Parent构造方法执行】
【Child构造方法执行】

=== 验证类加载器 ===
child的类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
child2的类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2
两个类加载器是否同一个实例: true

=== 详细比较 ===
loader1的hashCode: 414493378
loader2的hashCode: 414493378
loader1的类: sun.misc.Launcher$AppClassLoader
loader2的类: sun.misc.Launcher$AppClassLoader

为什么是同一类加载器?------原理验证

为什么是同一类加载器?------JVM内存结构验证

相关推荐
阿阿阿阿里郎1 小时前
ROS2快速入门--C++基础
开发语言·c++·算法
free-elcmacom1 小时前
C++<x>new和delete
开发语言·c++·算法
我命由我123452 小时前
Git 创建新分支并推送到远程仓库
java·服务器·git·后端·学习·java-ee·学习方法
程序喵大人2 小时前
map的[]运算符,这个看似方便的语法,藏着怎样的魔鬼?
开发语言·c++·map·运算符
全栈开发圈2 小时前
新书速览|R语言医学数据分析与可视化
开发语言·数据分析·r语言
014-code2 小时前
手把手带你解读 Dockerfile - 最快上手方法
java·docker·容器·持续部署
傻啦嘿哟2 小时前
爬虫跑了一小时还没完?换成列表推导式,我提前下班了
java·开发语言·jvm
xiaoye37082 小时前
Spring 动态代理源码深度分析
java·后端·spring
青槿吖2 小时前
第一篇:Spring面试高频三连问:容器区别|Bean作用域|生命周期,一篇拿捏!
java·开发语言·网络·网络协议·spring·面试·rpc