Class 文件长什么样
编译器把 .java 翻译成 .class,一个严格遵守格式的二进制文件:开头四个字节是魔数 0xCAFEBABE(JVM 认这个口令才往下读),接着是版本号、常量池、字段表、方法表……JVM 对来源不挑:磁盘文件、网络字节流、JAR 包、运行时动态生成(动态代理)都可以——只要格式对,从哪来不重要。这份宽容是 Java 生态繁荣的底座,也是字节码注入这类攻击的入口。
五步流水线
加载:读入字节流,在方法区生成类的运行时数据,堆里生成对应的 Class 对象。验证:魔数、格式、语义、字节码的安全性逐层检查,防止恶意字节码搞事。准备:给静态变量分配内存并设零值——注意 static int value = 10 在这一步是 0 不是 10,赋 10 的动作发生在初始化阶段;但 static final int VALUE = 10 是例外,final 常量在准备阶段就直接赋值(ConstantValue 属性)。解析:把常量池里的符号引用换成直接引用。初始化:执行 <clinit>——编译器把静态变量赋值语句和 static 块按源码顺序拼成的方法。
public class Config {
static Config INSTANCE = new Config(); // ① 先 new,此时 version=0
static int version = 1; // ② 再赋值,version=1
private Config() {
System.out.println("ctor sees version = " + version); // 输出 0!
}
}
// 构造函数在"初始化"进行到一半时被调用——字段顺序换了结果就换初始化的触发时机
初始化是"惰性"的,只有六种主动引用会触发:new、读写静态字段(非 final)、调用静态方法、反射调用、初始化子类时连带父类、main 所在的主类。对应地有被动引用:通过子类引用父类的静态字段(只初始化父类)、定义数组 User[] arr(不初始化 User)、引用编译期常量(已进常量池)。最经典的坑:把配置类当成"用就被加载",实际靠常量引用根本没触发初始化,排查半天发现逻辑压根没跑。
clinit 自带线程安全
// 静态内部类单例:为什么天生线程安全且懒加载
public class Holder {
private Holder() {}
private static class Inner {
static final Holder INSTANCE = new Holder();
}
public static Holder getInstance() {
return Inner.INSTANCE; // 第一次访问才触发 Inner 的初始化
}
}
// JVM 保证 <clinit> 只执行一次且全程持锁——
// 等于用类加载机制免费获得了双重检查锁的效果这个保证也有一体两面:如果某个类的 <clinit> 里写了耗时逻辑,第一个触发它的线程会把锁一直占着,其他线程全部阻塞——"静态块卡死全线程"是真实存在的事故形态。
一道顺序题收尾
面试常客:父类静态块、子类静态块、父类构造、子类构造的执行顺序?答案是静态先行(父先子后)、构造随后(父先子后)——因为子类初始化前必须先完成父类初始化,而 new 子类时构造函数第一行永远先调 super()。理解了五步流水线,这类题不用背。下一篇看加载这道工序的执行者——类加载器,以及那个著名的双亲委派模型。
评论 (0)