静态初始化块的执行顺序:类加载时的那些隐藏陷阱
静态初始化块的执行顺序:类加载时的那些隐藏陷阱
适读人群:Java中级开发者、有过初始化顺序困惑的后端工程师 | 阅读时长:约13分钟 | 文章类型:原理剖析+执行验证
开篇故事
去年有个应届生同学,加入团队不久,被一个奇怪的bug搞了整整一天。他在定义一个配置类:
public class AppConfig {
public static final String DB_URL = "jdbc:mysql://localhost:3306/" + DB_NAME;
public static final String DB_NAME = "mydb";
}他说,访问AppConfig.DB_URL得到的是"jdbc:mysql://localhost:3306/null",DB_NAME明明定义了,为什么是null?
他找到我,我看了一眼,问他:"你知道这两个字段谁先初始化?"
他说:"按声明顺序?不对,DB_URL用到了DB_NAME……"
就是这里——Java静态字段和静态初始化块的执行顺序,严格按照在源码中的书写顺序,从上到下依次执行。DB_URL定义在DB_NAME之前,所以初始化DB_URL时DB_NAME还是默认值null。
这个问题说起来简单,但初始化顺序涉及的坑远不止这一个。今天把类加载时的初始化顺序彻底梳理清楚。
一、类加载和初始化的触发时机
什么时候触发类初始化
Java虚拟机规范定义了6种会触发类初始化的情况(主动引用):
- 使用
new创建类的实例 - 访问类的静态字段(被final修饰且在编译期已知的常量除外)
- 调用类的静态方法
- 使用反射(
Class.forName("...")) - 初始化一个类,如果其父类还没有被初始化,先初始化父类
- JVM启动时指定的主类(包含
main方法的类)
不会触发初始化的情况(被动引用):
// 情况1:通过子类访问父类的静态字段,不会触发子类初始化
System.out.println(Child.PARENT_CONSTANT); // 只触发Parent初始化
// 情况2:访问编译期final常量,不触发类初始化
// 编译器把常量直接内联到调用处了
System.out.println(AppConfig.MAX_SIZE); // 如果MAX_SIZE是static final int = 100
// 编译后等价于:System.out.println(100); 完全不需要加载AppConfig
// 情况3:创建数组
AppConfig[] arr = new AppConfig[10]; // 不触发AppConfig初始化初始化的完整执行顺序
1. 加载父类(如果有)
2. 执行父类的静态字段初始化 + 静态初始化块(按书写顺序)
3. 加载本类
4. 执行本类的静态字段初始化 + 静态初始化块(按书写顺序)
(到这里,类初始化完成)
5. 创建实例时:执行父类构造函数
6. 执行本类的实例字段初始化 + 实例初始化块(按书写顺序)
7. 执行本类构造函数体二、核心原理深挖
静态字段与静态初始化块的执行顺序
关键规则:静态字段初始化和静态初始化块,按照它们在源码中出现的顺序执行,没有谁先谁后的特殊规定,就是书写顺序。
public class OrderTest {
static {
System.out.println("静态块1");
x = 10; // 可以给后面定义的字段赋值(写操作合法)
// System.out.println(x); // 但不能读(编译错误:非法前向引用)
}
static int x = 5; // x先被静态块赋值为10,然后被这里赋值为5
static {
System.out.println("静态块2,x=" + x); // 输出:5
}
}执行顺序:
- 静态块1执行:打印"静态块1",x被赋值为10
- 静态字段
x = 5执行:x被赋值为5(覆盖了块1里的赋值) - 静态块2执行:打印"静态块2,x=5"
这里有个细节:静态块里可以对后面定义的字段进行写操作,但不能读(叫"非法前向引用",编译错误)。
类初始化的线程安全保证
JVM规范保证类初始化是线程安全的:如果多个线程同时尝试初始化同一个类,只有一个线程会执行初始化,其他线程等待。
这个特性被用来实现线程安全的单例(静态内部类Holder模式):
public class Singleton {
// 私有构造
private Singleton() {}
// 静态内部类,只在getInstance()第一次被调用时才加载
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE; // 触发Holder类初始化,线程安全
}
}初始化顺序流程图
三、完整代码实现
代码一:全面验证初始化顺序
package com.laozhang.trap.staticinit;
/**
* 静态初始化顺序全面验证
* 运行前猜一下输出顺序,对比实际结果
*/
public class InitOrderTest {
public static void main(String[] args) {
System.out.println("=== main开始 ===");
System.out.println("Child.STATIC_FIELD = " + Child.STATIC_FIELD);
System.out.println("\n=== 创建Child实例 ===");
Child c = new Child();
System.out.println("\n=== 创建第二个Child实例 ===");
Child c2 = new Child(); // 静态初始化只执行一次
}
}
class Parent {
// 1. 静态字段初始化
static String parentStaticField = initParentStatic();
// 2. 静态块
static {
System.out.println("Parent静态块,parentStaticField=" + parentStaticField);
}
// 3. 实例字段初始化
String parentInstanceField = initParentInstance();
// 4. 实例块
{
System.out.println("Parent实例块,parentInstanceField=" + parentInstanceField);
}
// 5. 构造函数
Parent() {
System.out.println("Parent构造函数");
}
private static String initParentStatic() {
System.out.println("Parent静态字段初始化");
return "parent_static";
}
private String initParentInstance() {
System.out.println("Parent实例字段初始化");
return "parent_instance";
}
}
class Child extends Parent {
// 静态字段
static String STATIC_FIELD = initChildStatic();
// 静态块
static {
System.out.println("Child静态块,STATIC_FIELD=" + STATIC_FIELD);
}
// 实例字段
String childInstanceField = initChildInstance();
// 实例块
{
System.out.println("Child实例块,childInstanceField=" + childInstanceField);
}
// 构造函数
Child() {
// super()隐式在这里,已经在父类阶段执行
System.out.println("Child构造函数");
}
private static String initChildStatic() {
System.out.println("Child静态字段初始化");
return "child_static";
}
private String initChildInstance() {
System.out.println("Child实例字段初始化");
return "child_instance";
}
}
/*
预期输出(按顺序):
=== main开始 ===
Parent静态字段初始化
Parent静态块,parentStaticField=parent_static
Child静态字段初始化
Child静态块,STATIC_FIELD=child_static
Child.STATIC_FIELD = child_static
=== 创建Child实例 ===
Parent实例字段初始化
Parent实例块,parentInstanceField=parent_instance
Parent构造函数
Child实例字段初始化
Child实例块,childInstanceField=child_instance
Child构造函数
=== 创建第二个Child实例 ===
Parent实例字段初始化 ← 静态部分不再执行,只有实例部分重复
Parent实例块,parentInstanceField=parent_instance
Parent构造函数
Child实例字段初始化
Child实例块,childInstanceField=child_instance
Child构造函数
*/代码二:常见陷阱场景与修复
package com.laozhang.trap.staticinit;
/**
* 静态初始化的常见陷阱
*/
public class StaticInitTraps {
// ===== 陷阱1:字段声明顺序导致值不对 =====
static class ConfigWrong {
// DB_URL用到了DB_NAME,但DB_NAME在DB_URL之后声明
public static final String DB_URL = "jdbc:mysql://host:3306/" + DB_NAME;
public static final String DB_NAME = "mydb"; // DB_URL初始化时,这里还是null
public static void main(String[] args) {
System.out.println(DB_URL); // jdbc:mysql://host:3306/null !!
}
}
// 修复:把被依赖的字段放在前面
static class ConfigCorrect {
public static final String DB_NAME = "mydb"; // 先定义
public static final String DB_URL = "jdbc:mysql://host:3306/" + DB_NAME; // 再用
public static void main(String[] args) {
System.out.println(DB_URL); // jdbc:mysql://host:3306/mydb ✓
}
}
// ===== 陷阱2:循环依赖导致类初始化卡死(死锁)=====
// 不要在生产里写这样的代码,这里只是演示
static class ClassA {
static {
System.out.println("ClassA初始化开始");
// 访问ClassB,触发ClassB初始化
// ClassB初始化时又访问ClassA,但ClassA正在初始化中...
String b = ClassB.B_VALUE;
System.out.println("ClassA初始化完成");
}
static final String A_VALUE = "A";
}
static class ClassB {
static {
System.out.println("ClassB初始化开始");
String a = ClassA.A_VALUE; // 访问ClassA
System.out.println("ClassB初始化完成");
}
static final String B_VALUE = "B";
}
// 结果:ClassA看到ClassB正在初始化,ClassB看到ClassA正在初始化
// 单线程情况下,先初始化的那个会看到另一个的字段是默认值(null/0)
// 多线程情况下,可能真正死锁
// ===== 陷阱3:静态字段在静态块之后定义,块里的赋值被覆盖 =====
static class OverwriteTrap {
static {
value = 42; // 在字段声明前赋值
}
static int value = 0; // 这里的赋值覆盖了上面静态块的赋值
// 最终value是0,不是42
public static void print() {
System.out.println("value=" + value); // 0
}
}
// ===== 陷阱4:枚举的初始化顺序 =====
// 枚举也有初始化顺序问题
enum StatusEnum {
ACTIVE(1), INACTIVE(2);
private final int code;
// 这个Map在枚举常量初始化之后才初始化
// 如果在枚举构造中访问这个Map会出错
private static final java.util.Map<Integer, StatusEnum> CODE_MAP;
static {
// 到这里,ACTIVE和INACTIVE已经初始化完了
CODE_MAP = new java.util.HashMap<>();
for (StatusEnum e : values()) {
CODE_MAP.put(e.code, e);
}
}
StatusEnum(int code) {
this.code = code;
// 不能在这里访问CODE_MAP,因为CODE_MAP还没初始化
// CODE_MAP.put(code, this); // 这里会NPE!
}
public static StatusEnum fromCode(int code) {
return CODE_MAP.get(code);
}
}
// ===== 正确的配置类设计 =====
static class AppConfig {
// 方案1:所有字段按依赖顺序排列
private static final String HOST = "localhost";
private static final int PORT = 3306;
private static final String DB = "mydb";
private static final String URL = String.format("jdbc:mysql://%s:%d/%s", HOST, PORT, DB);
// 方案2:用静态初始化块做复杂初始化
private static final java.util.Properties props;
static {
props = new java.util.Properties();
try {
props.load(AppConfig.class.getResourceAsStream("/app.properties"));
} catch (Exception e) {
throw new ExceptionInInitializerError("配置文件加载失败: " + e.getMessage());
}
}
public static String getUrl() { return URL; }
public static String getProperty(String key) { return props.getProperty(key); }
}
public static void main(String[] args) {
System.out.println("=== 字段顺序陷阱 ===");
System.out.println("错误: " + ConfigWrong.DB_URL);
System.out.println("正确: " + ConfigCorrect.DB_URL);
System.out.println("\n=== 静态块覆盖陷阱 ===");
OverwriteTrap.print();
System.out.println("\n=== 枚举初始化 ===");
System.out.println(StatusEnum.fromCode(1));
}
}四、踩坑实录
坑1:静态字段依赖顺序写反,值是null
报错现象:
启动时没有报错,但运行时某个静态配置值是null,导致NullPointerException。
java.lang.NullPointerException: Cannot invoke "String.concat(String)" because "AppConfig.DB_NAME" is null
at com.example.AppConfig.<clinit>(AppConfig.java:5)注意:这个异常是<clinit>(类初始化方法)里的,说明是静态初始化时出的问题。
根本原因:
static String A = "prefix_" + B; // B还没初始化,是null
static String B = "value";具体解法:
把被依赖的字段放在前面,或者用静态初始化块显式控制顺序:
// 方案1:调整顺序
static String B = "value";
static String A = "prefix_" + B;
// 方案2:静态块
static String A;
static String B;
static {
B = "value";
A = "prefix_" + B;
}坑2:静态初始化块抛出异常导致类加载失败
报错现象:
java.lang.ExceptionInInitializerError
Caused by: java.io.FileNotFoundException: /config/app.properties (No such file or directory)
at com.example.AppConfig.<clinit>(AppConfig.java:15)后续所有访问该类的代码都报:
java.lang.NoClassDefFoundError: Could not initialize class com.example.AppConfig根本原因:
静态初始化块里抛了未捕获的异常,JVM把这个异常包装成ExceptionInInitializerError。类初始化失败后,该类被标记为"不可用",后续所有对该类的访问都抛NoClassDefFoundError。
具体解法:
static {
try {
props = loadProperties();
} catch (IOException e) {
// 方案1:用默认值
props = new Properties();
log.warn("配置文件未找到,使用默认配置");
// 方案2:包装成ExceptionInInitializerError(有意中止类加载)
// throw new ExceptionInInitializerError("配置文件缺失: " + e.getMessage());
}
}坑3:多线程下的静态初始化死锁
报错现象:
服务启动时卡住,没有任何输出,线程dump显示多个线程处于BLOCKED状态,等待同一个锁。
根本原因:
两个类互相依赖对方的静态初始化,在多线程环境下各自持有自己类的初始化锁,等待对方的初始化完成,形成死锁。
JVM的类初始化锁保证了同一个类只会被初始化一次,但如果两个类的初始化互相等待,就死锁了。
具体解法:
消除类之间的循环静态依赖。通常是通过提取公共类、延迟初始化、或重构设计来解决。
// 把两个类都依赖的常量提取到独立类
class SharedConstants {
static final String COMMON = "shared_value";
}
class ClassA {
static final String A = SharedConstants.COMMON + "_a"; // 只依赖SharedConstants
}
class ClassB {
static final String B = SharedConstants.COMMON + "_b"; // 只依赖SharedConstants
}五、总结与延伸
静态初始化顺序,记住一个核心规则:父类在子类之前,同类内部按书写顺序。
实践建议:
1. 被依赖的字段放在使用它的字段之前
这是最直接的解法。让代码的阅读顺序和执行顺序一致,不会有歧义。
2. 复杂初始化逻辑放到静态初始化块里,并做异常处理
不要让静态块里的异常不加处理地向上冒泡,会导致类加载失败。
3. 避免类之间的循环静态依赖
如果A依赖B,B又依赖A,重新设计一下,把公共部分提取出来。
4. 利用JVM类初始化的线程安全特性实现单例
静态内部类Holder模式比双重检查锁(DCL)更简洁,且由JVM保证线程安全,是实现懒加载单例的首选方式。
5. 枚举的CODE_MAP不能在构造函数里填充
枚举常量在静态Map之前初始化,只能在静态块里填充Map。
