双亲委派模型
在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构造方法执行】
- Parent类的加载时机 :在加载Child类的过程中被触发,而不是在初始化阶段
- 加载顺序:Parent先被加载,Child后被加载
- 初始化顺序:Parent先初始化,Child后初始化
- 类加载器: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
静态方法调用
为什么只执行一次?
双亲委派模型通过以下机制保证了"只执行一次":
- 缓存机制:每个类加载器都有自己的命名空间,已加载的类会被缓存
- 委派机制:同一个类在JVM中只会被加载一次
- 初始化标志:每个类都有初始化状态标记,已初始化的类不会再次初始化
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内存结构验证
