变量值由加载、准备、初始化三阶段协同决定:加载只建结构不赋值;准备阶段分配内存并设默认值或编译期常量;初始化阶段执行赋予源码指定值。

掌握变量在类加载全周期中的状态变化,关键不是死记阶段名,而是盯住“谁在什么时候改了什么值”。变量的最终值,是加载、准备、初始化三个阶段协同作用的结果,中间存在明确的时间差和赋值逻辑断层。
加载阶段:只建结构,不碰值
这个阶段 JVM 只做三件事:找字节码(可以从 class 文件、jar、网络甚至动态生成)、转成方法区的数据结构、生成一个 Class 对象。它完全不执行任何 Java 代码,也不给静态变量赋你写的初值。
- 比如
public static int x = 123;,此时 x 还没被分配内存,更别说设成 123 - 哪怕写的是
public static final String s = "hello";,只要不是编译期常量(比如含表达式或运行时计算),也一样不会在这步赋值
准备阶段:分配内存 + 设零值/编译期常量
这才是变量第一次在内存里“有位置”的时刻。JVM 为所有类变量(static 修饰的变量)在方法区分配空间,并按类型设默认初始值:
- 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 Object obj = new Object(); 中的 obj 在准备阶段仍是 null,new 操作留到初始化阶段执行。
初始化阶段:执行 ,真正赋你写的值
这是唯一执行 Java 代码的类加载阶段。JVM 会收集所有静态变量赋值语句和静态代码块,按源码顺序合并成一个特殊方法 ,并保证多线程下只执行一次。
-
public static int x = 123;→ 这行会被编译进,此时 x 才从 0 变成 123 -
public static final String s = UUID.randomUUID().toString();→ 因为不是编译期常量,也会等到这一步才执行构造和赋值 - 父类
总是先于子类执行,所以父类静态字段一定先完成初始化
实战判断口诀:看赋值动作落在哪一环
遇到变量值疑问,直接对照三阶段定位:
- 如果变量是
static final+ 字面量(如100、"ok"、2+3),值在准备阶段就定死了 - 如果变量是
static但没 final,或 final 但值需运行时算出,赋值动作都在初始化阶段 - 如果变量没 static,属于实例变量,它的生命周期跟对象绑定,跟类加载无关,只在 new 对象时走自己的初始化流程
搞清这个演进链条,SPI 加载失败、静态字段空指针、热部署值不更新等问题,基本都能顺藤摸瓜找到根因。










