当前位置: 首页 > news >正文

第2章-类加载子系统-知识补充

第2章-类加载子系统-知识补充

补充真实案例来帮助理解类加载机制的过程。

1-类加载过程

在Java中,类加载过程分为:

  1. 加载(Loading)
  2. 验证(Verification)
  3. 准备(Preparation)
  4. 解析(Resolution)
  5. 初始化(Initialization)

✅ 以你给出的类为例:

public class Persson {private static final String PERSON_PREFIX = "人类";private static final Int PERSON_MAX_AGE = 150;private String name;private int age;
}

一、类加载过程(简化说明)

1. 加载(Loading)
  • JVM 使用 类加载器(ClassLoader) 加载 Persson.class 文件。
  • 类信息(类名、字段、方法、常量池等)被加载到 方法区(Method Area)(在 HotSpot 中,JDK8+ 是 元空间 Metaspace)。
2. 验证(Verification)
  • 验证字节码是否符合 JVM 规范。
3. 准备(Preparation)
  • 类变量(static 字段) 分配内存并设置 默认值
  • 注意:final 的常量(如 PERSON_PREFIXPERSON_MAX_AGE)在准备阶段就会被赋值为 编译期确定的常量值
4. 解析(Resolution)
  • 将符号引用解析为直接引用(如类、方法、字段的引用)。
5. 初始化(Initialization)
  • 执行 <clinit>() 方法(类构造器),初始化静态变量和静态代码块。
  • 由于你的类中所有 static 字段都是 final 且编译期可确定值,不会生成 <clinit>() 方法

二、运行时数据区(Runtime Area)分配

项目存储位置说明
类元信息(类名、字段、方法、访问修饰符等)方法区(Metaspace)类加载后,类的结构信息存储在方法区。
常量池(如字符串常量 “人类”)方法区(Metaspace)字符串字面量 "人类" 会进入 运行时常量池,并在字符串池中创建对应对象。
static final 基本类型常量(如 PERSON_MAX_AGE = 150)方法区(Metaspace)编译期常量,直接嵌入到使用处,不占用额外空间。
实例字段(如 name, age)Java 堆(Heap)每次 new Persson() 时,这些字段作为对象的一部分存储在堆中。

三、总结一句话

类信息、常量池、static final 常量 存储在 方法区(Metaspace)实例字段 存储在 堆(Heap) 中,方法调用 使用 Java 栈(JVM Stack)


🔍 补充说明:字符串常量 "人类"

  • 字符串常量 "人类" 会被放入 字符串常量池(String Pool),这是 堆中的一部分(JDK7+ 开始从方法区移到堆)。
  • 所以 "人类" 实际是 堆中的一个 String 对象,但引用存储在 运行时常量池(方法区)。

2-类加载过程

public class HelloLoader {public static void main(String[] args) {System.out.println("谢谢ClassLoader加载我....");System.out.println("你的大恩大德,我下辈子再报!");}
}

当 JVM 执行 java HelloLoader 时,类的生命周期从“被加载”到“被卸载”一共要经过 7 大阶段。下面把镜头对准 HelloLoader.class,完整走一遍这 7 个阶段,并指出每一步在内存里到底留下了什么痕迹。阅读时可以把“方法区”自动替换成“元空间(Metaspace)”,因为 HotSpot 8 以后就没有永久代了。


  1. Loading(加载)

  1. 启动类加载器(Bootstrap ClassLoader)先把 rt.jar 里的 java/lang/Objectjava/lang/Systemjava/io/PrintStream 等一股脑装进方法区。
  2. 用户类路径(-cp 或 CLASSPATH)下的 HelloLoader.class应用类加载器(AppClassLoader) 负责读取字节码。
  3. 读完之后,JVM 在 方法区 生成一个 InstanceKlass 结构(C++ 对象),里面装着:
    • 魔数、版本号、常量池(ConstantPool)
    • 字段表、方法表、访问标志(ACC_PUBLIC、ACC_STATIC 等)
    • 字节码指令数组(main 方法对应的 Code 属性)
  4. 同时在 Java 堆 里创建一个 java.lang.Class<HelloLoader> 镜像对象,作为 InstanceKlass 的“Java 视图”,后面所有 HelloLoader.class 都指它。
  5. 把类加载器(AppClassLoader)记录到该 InstanceKlass 的“类加载器”字段里,形成“加载器-类”双向绑定。

  1. Verification(验证)

  • 文件格式验证:魔数 0xCAFEBABE、主次版本号是否支持。
  • 元数据验证:检查 main 方法签名是否合规(public static void main(String[]))。
  • 字节码验证:用类型推导(StackMapTable)保证操作数栈不会溢出、不会把 int 当引用用。
  • 符号引用验证:看看 java/lang/System.out:Ljava/io/PrintStream; 在 rt.jar 里能不能找到。

  1. Preparation(准备)

  • 只为 类变量(static 字段) 分配内存并设 零值
  • HelloLoader 里没有 static 字段,所以这一步只是 把常量池里的符号引用“标好号”,等待下一步解析。
  • 注意:方法区里已经给 ConstantPool 分配了内存,但还没把 CONSTANT_MethodrefCONSTANT_String 等符号变成真正的指针。

  1. Resolution(解析)

  • 把常量池的符号引用“翻译”成 直接引用(内存地址或句柄)。
    java/lang/System.out 解析成 PrintStream 对象的 字段地址
    java/io/PrintStream.println(Ljava/lang/String;)V 解析成 方法指针
  • 字符串字面量 "谢谢ClassLoader加载我...." 被解析为 堆中 String 对象(StringTable 驻留)。
  • 类、接口、字段、方法、方法类型、方法句柄、调用点限定符 7 类常量全部“转正”。

  1. Initialization(初始化)

  • 执行 <clinit>() 方法。HelloLoader 没有 static 变量,也没有 static 代码块,因此 不会生成 <clinit>(),这一步直接跳过。
  • JVM 规定:初始化阶段必须“线程安全”,内部用 ClassLoader 对象做同步锁,保证多线程下只初始化一次。

  1. 执行 main 方法

  1. JVM 在 当前线程的 Java 栈 里压入一个 栈帧(Stack Frame),局部变量表只有一项:args。
  2. 解释器(或 JIT 编译后的汇编)开始执行字节码:
    getstatic #2  // System.out
    ldc       #3  // "谢谢ClassLoader加载我...."
    invokevirtual #4 // PrintStream.println(String)
    
  3. 两条 println 指令执行完后,栈帧弹出,main 方法返回。
  4. 字符串常量 "谢谢ClassLoader加载我....""你的大恩大德,我下辈子再报!" 此时已在 堆的 StringTable 中驻留,且被 PrintStream 引用过一次。

  1. Unloading(卸载,可选)

  • HelloLoader类加载器实例(AppClassLoader) 被 GC 判定为“不可达”时,它加载的所有类才会被卸载。
  • 对于单次 java HelloLoader 进程,主线程结束、JVM 退出,整个方法区和堆都被操作系统回收,卸载逻辑由操作系统一次性完成,无需 JVM 级卸载。

一张图总结内存布局

方法区(Metaspace)
├── InstanceKlass<HelloLoader>
│   ├── ConstantPool(14 项常量:类/方法/字符串引用)
│   ├── method[1] = {name=main, access=ACC_PUBLIC|ACC_STATIC, code=...}
│   └── ...
└── 无 <clinit>(因为无 static 字段/代码块)Java 堆
├── java.lang.Class<HelloLoader>  (Class 镜像对象)
├── java.lang.String("谢谢ClassLoader加载我....")  (StringTable 驻留)
├── java.lang.String("你的大恩大德,我下辈子再报!")
└── PrintStream 对象(System.out 引用自 rt.jar)Java 栈(线程私有)
└── main 栈帧├── 局部变量表:args└── 操作数栈:执行 getstatic/ldc/invokevirtual 时进/出栈

一句话回顾

HelloLoader 被 AppClassLoader 装进方法区 → 常量池符号解析成直接引用 → 字符串入堆并驻留 → 主线程栈帧执行 main → 进程退出后方法区与堆一起被操作系统回收。


3-类加载过程

public class HelloApp {private static int a = 1; public static void main(String[] args) {System.out.println(a);}
}

把镜头继续对准 HelloApp,这一次类里 终于有了 static 变量
从“类加载”到“main 方法执行”的完整 7 阶段,重点看 准备、初始化 两步的内存变化,以及 () 的生成与执行。


  1. 前置约定

  • 方法区 ≈ Metaspace(C++ 侧的 InstanceKlass
  • 类变量(static)≠ 实例变量;前者在方法区,后者在对象里(堆里)
  • <clinit>() 由编译器 自动合成,专门用来执行 static 变量赋值 + static 代码块

  1. Loading(加载)

AppClassLoader 读取 HelloApp.class,在 Metaspace 创建 InstanceKlass<HelloApp>,并在堆生成 java.lang.Class<HelloApp> 镜像对象。
此时 a 的字面量“1”还躺在常量池里,字段表只标记了:

name=a, descriptor=I, access=ACC_PRIVATE|ACC_STATIC|ACC_FINAL(no)

  1. Verification(验证)

常规检查:魔数、版本号、字节码类型安全、符号引用是否存在。
getstatic java/lang/System.out / invokevirtual PrintStream.println(I) 等引用解析前会被校验。


  1. Preparation(准备)—— 关键一步

  • 为类变量 a 分配内存(在 InstanceKlass静态字段块 里,Metaspace 背后是一整块 C++ 内存)。
  • 赋“零值” → 此时 a 的值是 0,不是源代码里的 1!
    (int 的零值是 0,引用是 null,boolean 是 false)
  • 源代码里的“1”仍躺在常量池的 ConstantInteger_info 里,尚未生效

  1. Resolution(解析)

把常量池里的符号引用翻译成直接引用:

  • java/lang/System.out → 真正的 PrintStream 对象地址
  • Methodref HelloApp.a:I → 字段地址(指向 Metaspace 静态区)
  • String_info / Methodref println(I) 等同样被解析。

  1. Initialization(初始化)—— 重头戏

5.1 编译器自动生成 <clinit>() 字节码
因为存在“显式赋值语句”:

private static int a = 1;

编译器把它搬到 <clinit>() 里,等价于:

static {a = 1;
}

生成的字节码只有 2 条指令:

iconst_1
putstatic #2  // Field a:I
return

5.2 JVM 获取 ClassLoader 的对象锁,确保多线程下单例初始化。
5.3 执行 <clinit>()a 从 0 变成 1
5.4 把 InstanceKlass初始化标志位 设为 “fully initialized”,后续再也进不来。


  1. 执行 main 方法

  1. 当前线程 Java 栈压入新帧:局部变量表 args。
  2. 字节码序列:
getstatic #3  // System.out
getstatic #2  // HelloApp.a:I
invokevirtual #4 // PrintStream.println(int)
return
  1. 操作数栈:
    getstatic #3PrintStream 引用压栈
    getstatic #21 压栈(此时已不再是 0)
    invokevirtual 消费栈顶两个 slot,调用 println(int)
  2. 控制台输出:
1

  1. Unloading(卸载)

与上一个例子相同:只有当 AppClassLoader 本身不可达时,HelloAppInstanceKlass 和静态变量内存才会被 GC 回收。对于一次性 java HelloApp 进程,整个 JVM 退出即全部回收。


内存时间线一览

阶段a 的真实值存储位置说明
加载后未存在仅字段表
准备后0Metaspace 静态区零值规则
1Metaspace 静态区执行 putstatic
main 运行时1Metaspace 静态区getstatic 读取

一句话总结

准备阶段给 static 变量 a 分配内存并置 0;初始化阶段执行编译器生成的 <clinit>() 把 1 写进去;后续所有 getstatic 都从 方法区(Metaspace)里的静态字段块 拿到最新值。


4-类加载过程

public class ClinitTest {private int a = 1;public static void main(String[] args) {int b = 2;}
}

继续跟拍 ClinitTest,这次类里 只有实例变量 a 和局部变量 b,没有任何 static 字段或 static 代码块
因此:

  • 不会生成 ()(类初始化方法)
  • 只会生成 ()(实例初始化方法,供构造方法调用)
  • 局部变量 b 完全活在 Java 栈帧的局部变量表 里,跟堆/方法区无关

下面按 7 阶段走一遍,重点看 “没有类初始化” 时 JVM 在内存里干了啥。


  1. Loading(加载)

AppClassLoader 读取 ClinitTest.class,在 Metaspace 创建 InstanceKlass<ClinitTest>,字段表记录:

name=a, descriptor=I, access=ACC_PRIVATE  // 注意:没有 ACC_STATIC

常量池里只有 ClinitTest 自身、java/lang/Objectjava/lang/System 等符号引用。


  1. Verification(验证)

常规检查:字节码合法、栈映射表正确、符号引用存在。
因为 没有 static 字段,准备阶段无需赋零值。


  1. Preparation(准备)

  • 类变量(static)0 个 → 什么都不做,a 是实例字段,内存分配推迟到 new 对象 时。
  • 零值规则对 a 不适用;a 的“0”值要等 对象在堆里分配 时才出现。

  1. Resolution(解析)

把常量池里的符号引用转成直接引用:

  • java/lang/Object → 已加载的 InstanceKlass 地址
  • java/lang/System.outPrintStream 对象地址(虽然本例没用,但解析阶段仍可能提前)

  1. Initialization(初始化)

关键结论:

  • 编译器 不会生成 ()(因为无 static 字段 / static 代码块)
  • 因此初始化阶段 什么都不执行,直接标记 InstanceKlass 为 “fully initialized”
  • 实例字段 a 的赋值 推迟到构造方法(即 ()

  1. 执行 main 方法

6.1 线程 Java 栈压入 main 栈帧
局部变量表 slot 0:args
局部变量表 slot 1:b(int 2)

6.2 字节码

iconst_2
istore_1
return
  • iconst_2 把 2 压操作数栈
  • istore_1 存到局部变量表 slot 1 → b = 2
  • 全程没有访问堆/方法区,只在栈里完成

6.3 方法返回,栈帧弹出 → b 随帧销毁


  1. 对象创建场景(补充说明)

虽然 main 里没有 new ClinitTest(),但如果哪天代码里出现:

ClinitTest obj = new ClinitTest();

JVM 会:

  1. Java 堆 分配对象内存,对象头 + 实例字段 a
  2. 把 a 赋 零值(0)
  3. 调用 ()(实例初始化器),把 1 写进 obj.a
    对应字节码:
    aload_0
    invokespecial java/lang/Object.<init>
    aload_0
    iconst_1
    putfield ClinitTest.a:I
    return
    

内存分布小结

元素存储区域生命周期初始值来源
类元数据(ClinitTest)Metaspace与类加载器相同加载时建立
实例字段 aJava 堆(对象内)与对象相同new 时先 0, 再 1
局部变量 bJava 栈(帧内)方法调用期iconst_2 显式赋值

一句话总结

ClinitTest 没有 static 上下文,类初始化阶段空转;实例字段 a 的赋值延迟到 构造方法 ();局部变量 b 完全在 main 栈帧里自生自灭。


5-类加载过程

public class ClinitTest {private int a = 1;private static int c = 3;public static void main(String[] args) {int b = 2;}
}

这一次 ClinitTest既有实例变量 a,又有 static 变量 c,于是:

  • 会生成 () 负责 static 字段 c 的初始化
  • 会生成 () 负责实例字段 a 的初始化
  • main 里的局部变量 b 仍只在 栈帧 里存活

下面按 7 阶段把两条初始化路径彻底拆开。


  1. Loading(加载)

AppClassLoader 读入 ClinitTest.class,Metaspace 里建立 InstanceKlass,字段表新增两条记录:

a: I  private        // 实例字段
c: I  private static // 类字段

  1. Verification(验证)

常规字节码、符号引用检查,确保 putstatic c / putfield a 的类型安全。


  1. Preparation(准备)—— 关键差异点

  • 类变量 c
    – 在 Metaspace 的 静态字段块 分配内存
    – 赋 零值 → 此时 c = 0
  • 实例变量 a
    – 什么都不做,等 new 对象 时再处理

  1. Resolution(解析)

把常量池里的符号引用转成直接引用:
java/lang/Systemjava/lang/ObjectClinitTest.c:I 等全部“转正”。


  1. Initialization(初始化)—— () 登场

5.1 编译器自动生成 <clinit>() 字节码,等价于:

static {c = 3;
}

实际指令:

iconst_3
putstatic #5  // Field c:I
return

5.2 JVM 获取类初始化锁,确保多线程单例执行
5.3 执行完毕后 c 从 0 → 3
5.4 标记 InstanceKlass 为 fully initialized,后续再也进不来


  1. 执行 main 方法

6.1 线程 Java 栈压入 main 栈帧
局部变量表 slot 0:args
局部变量表 slot 1:b

6.2 字节码

iconst_2
istore_1
return
  • 与类/对象毫无交互,只在栈内完成
  • 方法返回后 b 随帧销毁

  1. 如果 new 一个对象(补充 路径)

虽然 main 里没写 new,但只要出现:

ClinitTest obj = new ClinitTest();

会触发:

  1. Java 堆 分配对象内存(含字段 a)
  2. 先把 a 填 零值 0
  3. 调用 () 实例初始化器:
    aload_0
    invokespecial java/lang/Object.<init>
    aload_0
    iconst_1
    putfield #3  // Field a:I
    return
    
    把 1 写进 obj.a
  4. 返回对象引用给 obj

内存时间线一览

阶段c 值a 值存储位置
准备后0Metaspace 静态区
3Metaspace 静态区
new 对象后30→1a 在堆内对象体

一句话总结

类初始化 <clinit>() 只负责 static 字段 c(0→3),实例字段 a 的赋值 延迟到构造方法 ();main 里的 b 依旧 纯栈内变量,与堆/方法区无交集。


6-类加载过程

public class ClassInitTest {private static int num = 1;private static int number = 10;// linking之prepare: number = 0 --> initial: 10 --> 20static {num = 2;number = 20;System.out.println(num); }public static void main(String[] args) {System.out.println(ClassInitTest.num);System.out.println(ClassInitTest.number);}
}

下面把 ClassInitTest 的完整生命周期拆开,重点看 “prepare 赋零值 → initial 赋初值 → static 块再改值” 的三级跳。


  1. 代码速览

public class ClassInitTest {private static int num  = 1;      // ① 编译期生成 <clinit> 赋值private static int number = 10;   // ② 同上static {                          // ③ 编译器按 **源码顺序** 拼到 <clinit> 里num = 2;number = 20;System.out.println(num);      // 打印 2}public static void main(String[] args) {System.out.println(ClassInitTest.num);    // 2System.out.println(ClassInitTest.number); // 20}
}

  1. Loading(加载)

AppClassLoader 把 ClassInitTest.class 装进 Metaspace,生成 InstanceKlass,字段表:

num:I    private static
number:I private static

常量池里除了类名、字段名,还多了 java/lang/System.out 以及 println(I) 的符号引用。


  1. Verification(验证)

常规字节码、符号引用检查,确保 putstatic num/number 类型正确。


  1. Preparation(准备)—— 零值阶段

  • 在 Metaspace 静态变量块 分配内存
  • 按类型赋零值 → 此时
    num    = 0
    number = 0
    
  • 源代码里的 1 / 10 还躺在常量池 ConstantInteger_info 里,尚未生效

  1. Resolution(解析)

System.outprintln(I) 等符号引用解析成直接引用(指针/句柄)。


  1. Initialization(初始化)—— () 一条龙

编译器把 “字段赋值 + static 块”源码出现顺序 拼成 <clinit>()

0: iconst_1
1: putstatic #2  // num = 1
4: bipush 10
6: putstatic #3  // number = 10
9: iconst_2
10: putstatic #2 // num = 2
13: bipush 20
15: putstatic #3 // number = 20
18: getstatic #4  // System.out
21: getstatic #2  // num
24: invokevirtual #5 // println(int)
27: return

执行流程:

  1. num 0→1→2
  2. number 0→10→20
  3. 控制台输出 2(static 块里的 println)

  1. 执行 main

main 栈帧里两条 getstatic 分别读取:

num    = 2
number = 20

所以打印:

2
20

  1. 卸载

AppClassLoader 不可达时,该类与静态变量内存被 GC 回收;单次 java ClassInitTest 进程退出即整体回收。


内存时间线速查

阶段numnumber说明
prepare 后00零值规则
前半110字段赋值
后半220static 块覆盖
main 读取220最终可见值

一句话总结

prepare 只给 零值,initial 按 源码顺序 执行 <clinit>():先字段赋值(1/10),再 static 块覆盖成(2/20),因此 main 永远拿到的是 最后一版


7-类加载过程

public class ClinitTest {// 任何一个类声明以后,内部至少存在一个类的构造器private int a = 1;private static int c = 3;public static void main(String[] args) {int b = 2;}public ClinitTest() {a = 10;int d = 20;}
}

把镜头对准 ClinitTest,这一次重点区分 类初始化 <clinit>()实例初始化 <init>() 的两条独立赛道:

  • <clinit> 只干 static 的事(c = 3)
  • <init>实例 的事:先给字段 a 赋默认值 0 → 字段显式赋值 a=1 → 构造器里 a=10
  • 局部变量 b、d 永远只在 栈帧 里存活

下面按 7 阶段拆给你看。


  1. Loading(加载)

AppClassLoader 把 ClinitTest.class 装进 Metaspace,生成 InstanceKlass,字段表:

a:I  private        // 实例字段
c:I  private static // 类字段

方法表包含:

<clinit>()V        // 编译器自动生成
<init>()V          // 对应无参构造器
main([Ljava/lang/String;)V

  1. Verification(验证)

常规字节码/符号引用检查,确保 putstatic cputfield a 类型正确。


  1. Preparation(准备)

  • 类变量 c
    – 在 Metaspace 静态区分配内存 → 零值 c = 0
  • 实例变量 a
    – 什么都不做,等 new 对象 时再处理

  1. Resolution(解析)

java/lang/Objectjava/lang/System 等符号引用解析成直接引用。


  1. Initialization(类初始化)—— () 只做 static

编译器生成的 <clinit>() 等价于:

static {c = 3;
}

字节码:

iconst_3
putstatic #2  // Field c:I
return

执行后 c 从 0 → 3,类状态标为 fully initialized。


  1. 执行 main 方法

main 栈帧:

slot 0: args
slot 1: b

字节码:

iconst_2
istore_1
return

纯栈内操作,与堆/方法区无交集;方法结束 b 被弹出。


  1. 如果 new 一个对象—— () 赛道启动

示例代码:

ClinitTest obj = new ClinitTest();

JVM 内部顺序:

  1. Java 堆 分配对象内存(含字段 a)
    填零值 → a = 0
  2. 调用 ()(实例初始化器),编译器按 源码顺序 拼接:
    aload_0
    invokespecial java/lang/Object.<init>  // 父构造
    aload_0
    iconst_1
    putfield #3  // a = 1  (字段显式赋值)
    aload_0
    iconst_10
    putfield #3  // a = 10 (构造器里再覆盖)
    iconst_20
    istore_2     // d = 20 (局部变量,仅在此帧)
    return
    
    结果:obj.a 的最终值是 10
  3. 返回对象引用给 obj

内存时间线一览

阶段c 值a 值(对象未创建前)a 值(对象创建后)存储位置
prepare 后0Metaspace 静态区
3Metaspace 静态区
new 对象 先30(零值)0Java 堆
字段赋311Java 堆
构造器31010Java 堆
局部变量 d栈帧(随 结束销毁)

一句话总结

<clinit>() 只负责把 static c 从 0 → 3;真正的对象字段 a 要经历 零值→1→10 的三级跳,全在 () 里完成;局部变量 b、d 永远 栈内自生自灭


8-类加载过程

public class ClinitTest1 {static class Father {public static int A = 1;static {A = 2;}}static class Son extends Father {public static int B = A;}public static void main(String[] args) {// 加载Father类,其次加载Son类。System.out.println(Son.B); // 2}
}

这题是经典的 “父类 先于子类 执行” 场景。
只要记住一条 JVM 规则:

首次主动使用某个类时,它的全部祖先类会被自上而下、递归地初始化,且每个类只会初始化一次。

下面把 Son.B 这一行触发的完整链路拆开给你看。


  1. 触发点 —— 主动使用

main 里第一次 读取 Son.B,属于 “读取类的静态字段”,是 主动使用(JLS 5.4.3)。
于是 JVM 开始 初始化 Son;初始化 Son 之前必须 先初始化 Father(因为 Son extends Father)。


  1. 初始化顺序

2.1 先执行 Father 的 ()
编译器把“字段赋值 + static 块”按源码顺序拼成:

iconst_1
putstatic #2  // Father.A = 1
iconst_2
putstatic #2  // Father.A = 2
return

结果:Father.A = 2
(控制台无输出,因为 Father 里没有 println)

2.2 再执行 Son 的 ()
只有一行赋值:

getstatic #2  // Father.A(此时已是 2)
putstatic #3  // Son.B = Father.A
return

结果:Son.B = 2


  1. 类加载/初始化时序图

Loading (按需自顶向下)
Father.class -> Son.class↓              ↓
Verification  Verification↓              ↓
Prepare       Prepare
Father.A=0    Son.B=0↓              ↓
Resolution    Resolution↓              ↓
Initialization
Father.<clinit>()  →  A=1→2↓
Son.<clinit>()     →  B=A=2

  1. 控制台输出

2

与代码注释完全一致。


  1. 如果反过来写 —— 反例验证

若把 Father.A 的 static 块改成:

static { A = B; }   // 编译错误:非法前向引用

static { A = 2; System.out.println(Son.B); } // 非法:Son 尚未初始化

都会直接 编译期报错运行时 IllegalAccessError,因为 JVM 强制 父类 完成之前子类尚未初始化


  1. 卸载时机

两类的加载器都是 AppClassLoader,只有当该加载器不可达时才会被 GC 卸载;单次 java ClinitTest1 进程退出即整体回收。


一句话总结

先父后子 是铁律:Father 的 <clinit> 把 A 从 1→2 后,Son 的 <clinit> 才用 A 给 B 赋值,因此 Son.B 永远是 2


9-类加载过程

public class DeadThreadTest {public static void main(String[] args) {Runnable r = () -> {System.out.println(Thread.currentThread().getName() + "开始");DeadThread dead = new DeadThread();System.out.println(Thread.currentThread().getName() + "结束");};Thread t1 = new Thread(r, "线程1");Thread t2 = new Thread(r, "线程2");t1.start();t2.start();}
}class DeadThread {static {if (true) {System.out.println(Thread.currentThread().getName() + "初始化当前类");while (true) {// This loop is intentional to demonstrate a deadlock scenario}}}
}

这段代码是教科书级的 “类初始化死锁” 示例:
利用 JVM 同一个类初始化锁<clinit>() 锁)的 可重入 + 互斥 特性,让第一个抢到锁的线程在 static 块里写死循环,第二个线程就 永久阻塞 在类初始化阶段,连 main 都结束不了。

下面把底层锁机制、线程堆栈、以及为什么“停不下来”一次说清。


  1. 触发点 —— 首次 new DeadThread()

两个线程都执行到

DeadThread dead = new DeadThread();

属于 “主动使用” → 必须初始化 DeadThread 类 → 必须执行其 <clinit>()


  1. JVM 的类初始化锁规则(JDK 8+ 依旧如此)

  • 每个类对应 一把全局 Monitor,由 JVM 内部实现(不是 synchronized(XXX.class) 那种用户可见锁)。
  • 同一个类加载器下,类的 <clinit>() 只能被一个线程执行,其余线程阻塞在锁上
  • 锁是 可重入的:如果线程已持有锁,再次进入同类静态域/方法不会死锁;但 别的线程绝对进不来

  1. 时间线(假设 t1 先抢到锁)

t1: 进入 DeadThread.<clinit>()↓ 打印
"线程1初始化当前类"↓ 进入 while(true) { }↓ 永不退出,也 **不释放** 类初始化锁t2: 跑到 new DeadThread()↓ 尝试获取 DeadThread 的 <clinit> 锁↓ 锁已被 t1 占有 → **阻塞在 JVM 内部 monitor**↓ 表现为
java.lang.Thread.State: **RUNNABLE**  // 底层其实是 native 阻塞,JVM 把状态抬成了 RUNNABLEat DeadThread.<clinit>(DeadThreadTest.java:19)at DeadThreadTest.lambda$main$0(DeadThreadTest.java:7)at java.lang.Thread.run(Thread.java:750)

jstack 可以看到 t2 的栈顶停在

- waiting to lock <0x00000000d5c8c0a0> (a java.lang.Class for DeadThread)

  1. 现象总结

  • 控制台必现:
    线程1开始
    线程1初始化当前类
    
    之后 程序永远挂起,CPU 100% 由 t1 的 while(true) 吃掉。
  • t2 既不会打印“线程2开始”,也不会打印“初始化当前类”,卡在类初始化锁
  • jstack 会看到 t1 状态 RUNNABLE(死循环),t2 也是 RUNNABLE(等锁,JVM 把 native 阻塞标成 RUNNABLE)。
  • 强制 Ctrl-C 才能结束进程。

  1. 如何验证“类锁”而不是对象锁

把代码改成:

synchronized (DeadThread.class) {   // 用户态 Class 对象锁while(true);
}

此时 t1 与 t2 都 不会阻塞在类初始化阶段,而是阻塞在 用户态 synchronized,现象类似但栈信息不同;而原例的锁是 JVM 内部 monitor,栈里不会出现 synchronized 字样。


  1. 与“单例双重检查”类锁死锁的区别

  • 单例双重检查使用 用户可见的 synchronized(Class)static synchronized 方法,栈会显示 monitor enter
  • 本例是 JVM 自动插入的 <clinit>,栈只停在 <clinit>,没有 monitor 字样。

  1. 结论一句话

第一个线程把类初始化锁占住并死循环,第二个线程永远进不了 <clinit>(),于是整个进程卡死——这就是“类初始化死锁”的最简演示。


http://www.dtcms.com/a/592561.html

相关文章:

  • Go Fiber 简介
  • 专业酒店设计网站建设手机什么网站可以设计楼房
  • 20251110给荣品RD-RK3588开发板跑Rockchip的原厂Android13系统时熟悉散热风扇
  • UniApp自定义Android基座原理及流程
  • Ganache-CLI以太坊私网JSON-RPC接口执行环境搭建
  • Android 系统超级实用的分析调试命令
  • 【ZeroRange WebRTC】WebRTC 加密安全总览:对称/非对称、数字签名、证书、SHA/HMAC、随机数
  • 【ZeroRange WebRTC】数字签名与 WebRTC 的应用(从原理到实践)
  • 承德网站制作公司做国外的网站有什么不用钱的
  • 破解遗留数据集成难题:基于AWS Glue的无服务器ETL实践
  • Rust 的所有权系统,是一场对“共享即混乱”的编程革命
  • 【Rust 探索之旅】Rust 库开发实战教程:从零构建高性能 HTTP 客户端库
  • API 设计哲学:构建健壮、易用且符合惯用语的 Rust 库
  • 横沥镇做网站wordpress中文说明书
  • 先做个在线电影网站该怎么做贵阳做网站软件
  • 【字符串String类大集合】构造创建_常量池情况_获取方法_截取方法_转换方法_String和基本数据类型互转方法
  • Http请求中Accept的类型详细解析以及应用场景
  • 升鲜宝 供应链SCM 一体化自动化部署体系说明
  • grafana配置redis数据源预警误报问题(database is locked)
  • 拒绝繁琐,介绍一款简洁易用的项目管理工具-Kanass
  • 测试自动化新突破:金仓KReplay助力金融核心系统迁移周期缩减三周
  • 大语言模型入门指南:从科普到实战的技术笔记(1)
  • 大模型原理之Transformer进化历程与变种
  • 2025-简单点-ultralytics之LetterBox
  • 网站开发经济可行性分析石龙做网站
  • wordpress中国优化网络优化的目的
  • 【Linux网络】Socket编程TCP-实现Echo Server(下)
  • 路由协议的基础
  • ios 26的tabbar 背景透明
  • Hadoop大数据平台在中国AI时代的后续发展趋势研究CMP(类Cloudera CDP 7.3 404版华为鲲鹏Kunpeng)