静态代码块在类初始化(initialization)阶段执行,而非加载(loading)阶段;它被编译进jvm自动生成的线程安全的方法,同一classloader下每个类仅执行一次。

静态代码块不是“加载时”执行,而是“初始化时”执行
很多人看到“类加载时执行静态代码块”就误以为 ClassLoader.loadClass() 一调用,static 块就跑 —— 实际完全不是。JVM 的类生命周期分五步:加载(Loading)、验证(Verification)、准备(Preparation)、初始化(Initialization)、使用/卸载。只有到 Initialization 阶段,<clinit></clinit> 方法才会被触发,而 static 块正被编译进这个方法里。
这意味着:
-
Class.forName("X", false, cl)中第二个参数为false时,只加载不初始化,static 块不会执行 -
ClassLoader.loadClass("X")默认也不触发初始化,static 块安静待命 - 哪怕你反复
loadClass同一个类(同一 ClassLoader 下),只要没走到初始化,static 块就一动不动
<clinit></clinit> 是 JVM 自动生成的线程安全初始化方法
编译器会把类中所有 static 变量赋值语句和 static {} 块,按源码顺序合并成一个叫 <clinit></clinit> 的特殊私有静态方法。它不接受调用、不能重载、程序员无法显式写出来,但 JVM 在初始化阶段会自动执行它。
关键点在于:
- JVM 对每个类在每个 ClassLoader 下,保证
<clinit></clinit>最多执行一次 - 如果多个线程同时首次触发初始化(比如同时调用
MyClass.doStatic()),JVM 内部用类对象的 monitor 锁住该过程,仅一个线程执行,其余阻塞等待——无需你加synchronized - 一旦执行完成,JVM 将该类标记为 “已初始化”,后续任何访问都不再进入
<clinit></clinit>
“仅一次”是相对于类加载器 + 全限定名的组合而言的
所谓“只执行一次”,不是整个 JVM 进程里绝对唯一,而是指:同一个 ClassLoader 实例 + 同一个类全限定名 的组合下仅一次。
所以这些情况都会导致 static 块再次执行:
- Web 容器中热部署:旧 ClassLoader 卸载,新 ClassLoader 加载同名类 → 新的
<clinit></clinit> - 自定义 ClassLoader 每次 new 一个实例去 load 类 → 每个 ClassLoader 视为独立命名空间
- OSGi 或模块化系统中,不同模块导出同名类 → 实际是不同 Class 对象
你可以用 MyClass.class.getClassLoader() == MyClass.class.getClassLoader() 判断是否为同一上下文,但更直接的是打印 MyClass.class 的哈希值,不同 ClassLoader 加载的同名类,hashCode() 必然不同。
为什么不能靠 static 块做“应用级单次初始化”?
static 块适合做类粒度的一次性工作,比如初始化本类的 static final Map、注册本类的 JNI 函数。但它不适合承担跨类、跨模块、或需失败重试的初始化逻辑。
容易踩的坑包括:
- static 块里抛异常(如
IOException)会导致类初始化失败,之后所有对该类的主动使用都抛NoClassDefFoundError,且无法重试 - 依赖其他尚未初始化的类(比如循环依赖),可能触发意外的初始化顺序,甚至死锁
- 日志框架(如 Log4j)尚未 ready 时就试图打日志,造成静默失败或 NPE
- 在容器环境(Spring Boot)中,过度依赖 static 块初始化,会绕过 Bean 生命周期管理,破坏可测试性和配置灵活性
真正需要“进程内只跑一次”的逻辑,应交给 Spring 的 @PostConstruct、InitializingBean,或显式用 AtomicBoolean + 双检锁控制,而不是押注在类加载时机上。










