Java静态变量赋值分准备和初始化两阶段:准备阶段分配内存并设默认值(如int为0、引用为null),不执行任何代码;初始化阶段才执行<clinit>方法,按源码顺序完成显式赋值和静态块。

Java 中静态变量的赋值不是一次性完成的,而是分两个明确阶段:准备阶段设默认值,初始化阶段才执行你写的代码赋值。这个区分直接影响变量的实际取值,尤其在有依赖或跨类引用时容易出错。
准备阶段:只分配内存、只设默认值
当 JVM 完成类的加载(字节码读入、Class 对象创建)后,进入链接阶段的第一步——准备阶段。此时:
- JVM 在方法区为所有 static 变量 分配内存空间;
- 仅按数据类型赋予默认值:int/long/short/byte → 0,float/double → 0.0,char → '\u0000',boolean → false,引用类型 → null;
-
不执行任何 Java 代码,包括
static int x = 3;中的= 3,也跳过所有static{}块; - 唯一例外是
public static final编译期常量(如String字面量、基本类型字面量),它们的真实值会在准备阶段直接写入,不走后续初始化。
初始化阶段:执行 <clinit>,真正运行你的赋值逻辑
只有当类被主动使用时(比如访问非 final 静态字段、调用静态方法、new 实例、反射等),JVM 才触发初始化阶段。此时会执行一个由编译器自动生成的特殊方法 <clinit>:
- 它把源码中所有静态变量显式赋值语句和
static{}块,按从上到下、父类优先于子类的顺序合并; -
static int a = b + 1;和static int b = 5;写在一起时,a 实际得到的是0 + 1(因为 b 在此时尚未在<clinit>中被赋值); - 像
static List<String> cache = new ArrayList<>();或static String s = UUID.randomUUID().toString();这类含对象创建或方法调用的语句,全部在此阶段执行; -
<clinit>是 JVM 管理的私有方法,不能被调用、重载或反射执行,但可用javap -c ClassName查看其字节码。
为什么区分这两个阶段很重要
理解准备与初始化的分离,能解释很多看似反直觉的现象:
立即学习“Java免费学习笔记(深入)”;
- 即使一个类只被
ClassLoader.loadClass()加载而从未使用,它的 static 变量已在准备阶段获得默认值(如null或0),但static{}块不会运行; - 实例变量完全不参与这两个阶段——它们的内存分配和默认值设定发生在
new指令执行时,在堆上独立完成; - 父类的
<clinit>总是先于子类执行,这是实现“父类静态资源先就绪”的底层保障; - 多线程环境下,JVM 保证
<clinit>只执行一次且线程安全,避免重复初始化。
怎么验证这两个阶段的实际行为
最简单的方式是在类里加带日志的 static{} 块:
- 如果
main方法里只引用public static final int VAL = 100;,日志不会打印(编译期内联,跳过初始化); - 一旦访问
public static int x = 5;或调用任意静态方法,日志立刻输出——说明已进入初始化阶段; - 此时观察变量值:准备阶段设的默认值已被覆盖,但若在
static{}前就访问某变量(比如通过反射读取),看到的仍是默认值。


















