静态变量在准备阶段赋默认值,初始化阶段才执行赋值语句;static final基本类型或字符串字面量为编译期常量,在准备阶段直接确定,其余均在初始化阶段赋值。

静态变量在准备阶段赋默认值,初始化阶段才执行赋值语句
Java 类加载过程中,static 变量的“加载”不是一次性完成的动作,而是拆分在两个阶段:准备阶段(Preparation)和初始化阶段(Initialization)。很多人以为 static 字段声明时的赋值会在类加载开始就立刻执行,其实不然——它只在初始化阶段才真正运行。
准备阶段仅给 static 变量分配内存并设为默认值(如 int 是 0,Object 是 null),哪怕你写了 static int x = 42;,这个 42 也不会在这时写入。真正执行赋值逻辑、调用 static 块、触发 static 字段初始化代码,全都在初始化阶段发生。
- 只有当类首次被主动使用(如 new 实例、调用
static方法、访问非 finalstatic字段等)才会触发初始化 - 被动引用(比如子类引用父类的
static final字段)不会触发父类初始化 -
static final基本类型或字符串字面量,在准备阶段就直接“编译期常量”化,不走初始化阶段
static final 字段的特殊处理:编译期常量 vs 运行期常量
这是最容易踩坑的地方。如果一个 static final 字段是基本类型或 String,且用字面量直接初始化(如 static final int PORT = 8080;),Javac 会把它当作编译期常量,内联到所有引用处。此时即使你后续修改该字段值,不重新编译引用它的类,旧值依然生效。
但只要初始化过程涉及运行期计算,哪怕只是加个括号或调用 Integer.valueOf(),它就不再是编译期常量,而变成运行期常量,必须等到类初始化时才赋值。
立即学习“Java免费学习笔记(深入)”;
-
static final String NAME = "foo";→ 编译期常量,准备阶段即确定 -
static final String NAME = "foo".toLowerCase();→ 运行期常量,初始化阶段才执行 -
static final List<string> LIST = Arrays.asList("a");</string>→ 必须初始化阶段执行,因为构造发生在运行时 - 这种差异直接影响热部署、模块隔离、甚至单元测试中类加载顺序的稳定性
类初始化时机不等于类加载时机,ClassLoader 可能早于 init 阶段介入
一个类从磁盘读取字节码、解析成 Class 对象,属于加载(Loading)阶段;而初始化(Initialization)是整个生命周期中更靠后的一环。这意味着:你可以在初始化前,通过 ClassLoader.loadClass() 拿到 Class 对象,但此时所有 static 块和字段赋值都还没跑。
常见误用场景是想靠 static 块做单例预热或配置加载,却在类刚被 loadClass 后就认为“已经 ready”,结果访问字段时仍是默认值或 null。
-
Class.forName("X")默认会触发初始化;ClassLoader.loadClass("X")不会 - 反射获取
Class对象后,第一次调用X.class.getDeclaredField(...)不触发初始化;但X.class.getField(...)如果字段不可见,可能抛异常,但也不触发初始化 - 真正触发初始化的操作包括:
new X()、X.staticMethod()、X.STATIC_FIELD(非 final 或非编译期常量)
多个 static 块和字段的执行顺序严格按源码顺序
Java 规范明确要求:类初始化时,static 字段初始化与 static 块按它们在源文件中出现的**文本顺序**依次执行。这不是 JVM 实现差异,是强制语义。一旦顺序写错,尤其存在相互依赖时,很容易出现 null 或默认值陷阱。
比如你在 static A a = new A(); 下面定义 static B b = use(a);,那么 use(a) 看到的 a 是已构造完的对象;但如果把 b 定义在 a 前面,a 就还是 null。
- 字段声明中的表达式(如
static List<string> list = initList();</string>)和static{}块混排时,也严格按书写顺序执行 - 父类的
static初始化一定先于子类,无论子类有没有显式static内容 - IDE 有时自动排序字段会悄悄破坏初始化逻辑,建议关闭对
static成员的自动重排
真正难的不是记住哪个阶段干啥,而是当问题出现在类加载链深处(比如 Spring 的 BeanFactory 初始化、Agent 注入、模块系统隔离)时,你得快速判断:当前看到的 null 或默认值,到底是没走到初始化阶段,还是初始化失败被吞了异常,又或者根本是编译期常量导致的“假更新”。这时候看 javap -v 输出的 ConstantValue 属性和 clinit 方法体,比猜快得多。








