Java类静态变量的加载分准备和初始化两阶段:准备阶段设默认值或编译期常量,初始化阶段执行<clinit>方法完成显式赋值,且受JVM时序控制,父类优先、自上而下执行。

Java 类变量(即静态变量)的加载不是一蹴而就的,而是贯穿类加载全过程的三阶段协同结果:准备阶段设默认值、初始化阶段执行赋值逻辑、且全程受 JVM 严格时序控制。真正决定变量最终值的,是它在哪一阶段被赋予哪个值。
准备阶段:分配内存并设默认值或编译期常量
这是静态变量第一次“有位置”的时刻。JVM 在方法区为其分配空间,并按类型赋予默认值:
- 数值类型(int/long/short/byte)→ 0
- 浮点类型(float/double)→ 0.0
- char → '\u0000'
- boolean → false
- 引用类型(Object、String 等)→ null
唯一例外是 static final + 编译期常量(如 public static final int PORT = 8080; 或 public static final String MSG = "ok";),其真实值会在准备阶段直接写入,不进后续初始化流程,也不触发类初始化。
初始化阶段:执行 <clinit>,完成你写的赋值
这才是你代码中显式初始化语句真正起效的地方。JVM 把所有静态变量赋值和静态代码块,按源码自上而下顺序合并成一个特殊方法 <clinit>,并在类首次主动使用时自动、线程安全、仅执行一次。
立即学习“Java免费学习笔记(深入)”;
-
public static String s = UUID.randomUUID().toString();→ 调用发生在此阶段 -
public static List<String> cache = new ArrayList<>();→ new 操作在此阶段执行 - 若赋值调用了静态方法,该方法体内的代码会立即运行,再把返回值赋给变量
注意:<clinit> 不是你写的普通方法,不能被调用、重载或反射执行;可用 javap -c ClassName 查看其字节码指令序列。
继承体系下的执行顺序:父类优先,内部有序
当存在父子类时,静态初始化严格遵循“先父后子、各层内自上而下”的链条:
- 先执行父类全部静态变量赋值 → 父类全部静态代码块(穿插在对应变量声明位置)
- 再执行子类全部静态变量赋值 → 子类全部静态代码块(同样穿插)
例如父类中 static int a = 10; 后紧跟 static { System.out.println(a); },输出一定是 10;若后面还有 static int b = a + 5;,则 b 的值就是 15。顺序不可跳过、不可并发、不可重复。
什么操作会真正触发静态变量初始化?
关键不是“访问类”或“声明变量”,而是“类被初始化”。以下六种情况之一发生时,JVM 才启动初始化阶段:
- 用
new创建该类实例 - 调用该类的静态方法
- 读取或修改该类的非final静态字段
- 反射调用
Class.forName("X") - 初始化子类时,父类尚未初始化 → 先触发父类初始化
- 使用
java Xxx启动主类(即执行其main方法)
以下不会触发初始化:
- 访问
static final基本类型或字符串常量(如Math.PI) - 仅声明类对象引用:
Class<?> c = X.class; - 创建该类数组:
X[] arr = new X[10]; - 调用
ClassLoader.loadClass("X")(非forName)
不复杂但容易忽略


















