资讯动态

【DIY系列:Java虚拟机】第22篇:ConstantValue 与简单属性——static final 常量的秘密

发布时间:2026/9/14 22:05:13 来源:尧图企业网站定制
上一篇【第21篇】属性表概述——class 文件的百宝箱下一篇【第23篇】Code 属性——方法的灵魂摘要从简单的开始——本篇实现 4 种小属性ConstantValuestatic final常量的值、SourceFile源文件名、Deprecated过时标记、Synthetic编译器生成标记。这里面藏着一个有趣的知识点为什么static final int x 10的值存在 class 文件里而static int y 10的值却不在答案涉及类初始化的时机差异——前者是编译期常量后者需要执行clinit方法。另外Deprecated 和 Synthetic 是零长度属性只有头部没有内容这种标记型属性的设计也很有意思。一、ConstantValuestatic final 常量的值结构ConstantValue_attribute{u2 attribute_name_index;// → ConstantValueu4 attribute_length;// 固定为 2u2 constantvalue_index;// → 指向常量池常量值}【ConstantValue 的结构】 ┌──────────────────┬──────────────┬─────────────────────┐ │name_index │length │constantvalue_index │ │→ ConstantValue │ 2 (固定) │→ 常量池索引 │ └──────────────────┴──────────────┴─────────────────────┘ │ ▼ ┌──────────────────────────┐ │ Integer / Float / Long / │ │ Double / String │ └──────────────────────────┘ 注意attribute_length 固定为 2 因为内容就是 constantvalue_index 这一个 u2关键问题什么样的字段有 ConstantValue这是本文的核心知识点。看个对比publicclassConstDemo{publicstaticfinalintA10;// ✅ 有 ConstantValuepublicstaticintB10;// ❌ 没有publicfinalintC10;// ❌ 没有publicstaticfinalStringDhello;// ✅ 有 ConstantValuepublicstaticfinalObjectEnewObject();// ❌ 没有}字段声明有 ConstantValue 吗原因static final int A 10✅编译期常量基本类型static int B 10❌不是 final可以在运行期改final int C 10❌实例字段每个对象一份值在构造器里赋static final String D hello✅编译期常量String 字面量static final Object E new Object()❌值要运行时 new不是编译期常量重点只有static final修饰的、且值是编译期常量的字段才有 ConstantValue 属性。具体来说必须是static final类型必须是基本类型或String值必须是常量表达式字面量或字面量的简单运算为什么类初始化的时机差异这两类字段的赋值时机完全不同【两类字段的赋值时机】 ① 有 ConstantValue 的字段static final 常量 编译期值直接写进 class 文件的 ConstantValue 属性 类加载「准备」阶段JVM 直接从 ConstantValue 读出值赋给字段 ↓ ★【不需要执行任何代码】★ ② 没有 ConstantValue 的字段普通 static 字段 编译期赋值语句被编译进 clinit() 方法类构造器 类加载「初始化」阶段执行 clinit()才真正赋值 ↓ ★【需要执行代码】★ 类加载三阶段回顾 ┌──────────────────────────────────────────┐ │ 加载 Loading │ ├──────────────────────────────────────────┤ │ 链接 Linking │ │ ├─ 验证 Verification │ │ ├─ 准备 Preparation ← ① 在这里赋值 │ │ │ static 变量分配内存 赋【零值】 │ │ │ ★有 ConstantValue 的直接赋真实值★ │ │ └─ 解析 Resolution │ ├──────────────────────────────────────────┤ │ 初始化 Initialization ← ② 在这里赋值 │ │ 执行 clinit() 方法 │ └──────────────────────────────────────────┘看个实际例子理解这个差异的影响// ConstTest.javapublicclassConstTest{publicstaticfinalintA10;// 常量publicstaticintB10;// 静态变量}// Main.javapublicclassMain{publicstaticvoidmain(String[]args){System.out.println(ConstTest.A);// 不会触发 ConstTest 初始化System.out.println(ConstTest.B);// 会触发 ConstTest 初始化}}重点访问static final常量不会触发类初始化因为编译器把常量值直接内联到了调用处编译期就把ConstTest.A替换成了10。而访问普通static字段会触发类初始化执行clinit()。这就是编译期常量内联——也是为什么改了常量值后必须重新编译所有引用它的类否则用的还是旧值。这是个经典的生产事故来源Go 实现// ch03/classfile/attribute_info.go// ConstantValueAttribute 表示常量值属性typeConstantValueAttributestruct{constantValueIndexuint16}func(self*ConstantValueAttribute)readInfo(reader*ClassReader){self.constantValueIndexreader.readUint16()}// ConstantValueIndex 返回常量值索引func(self*ConstantValueAttribute)ConstantValueIndex()uint16{returnself.constantValueIndex}使用时通过常量池索引取得真正的值第 6 章会用到// 第 6 章实现类初始化时的用法提前预告func(self*Class)initStaticFinalVars(){for_,field:rangeself.fields{iffield.IsStatic()field.IsFinal(){ifvalAttr:field.ConstantValueAttribute();valAttr!nil{index:valAttr.ConstantValueIndex()slot:self.constantPool.GetConstant(index)field.PutStaticValue(slot)}}}}二、SourceFile源文件名结构SourceFile_attribute{u2 attribute_name_index;// → SourceFileu4 attribute_length;// 固定为 2u2 sourcefile_index;// → Utf8源文件名}// SourceFileAttribute 表示源文件属性typeSourceFileAttributestruct{cp ConstantPool sourceFileIndexuint16}func(self*SourceFileAttribute)readInfo(reader*ClassReader){self.sourceFileIndexreader.readUint16()}// FileName 返回源文件名func(self*SourceFileAttribute)FileName()string{returnself.cp.getUtf8(self.sourceFileIndex)}作用记录这个 class 文件是从哪个.java文件编译来的。用途异常堆栈里显示文件名和行号Exception in thread main java.lang.NullPointerException at HelloWorld.main(HelloWorld.java:5) ← 这里的文件名 ↑ 来自 SourceFile 属性调试器定位源码IDE 靠它找到对应的.java文件错误提示javac的编译错误、各种工具的错误信息注意如果没有这个属性比如用-g:none编译异常堆栈里就只显示类名没有文件名at HelloWorld.main(Unknown Source) ← 没有 SourceFile 属性时三、Deprecated 与 Synthetic零长度标记属性这两个属性只有头部没有内容attribute_length 0。结构Deprecated_attribute{u2 attribute_name_index;// → Deprecatedu4 attribute_length;// 固定为 0 ← 零长度}Synthetic_attribute{u2 attribute_name_index;// → Syntheticu4 attribute_length;// 固定为 0 ← 零长度}【零长度属性的结构】 ┌──────────────────┬──────────────┐ │name_index │length 0 │ │→ Deprecated │ │ └──────────────────┴──────────────┘ ↑ ↑ 属性名 没有内容 这种属性的【存在本身】就是信息 → 有这个属性 true → 没这个属性 falseGo 实现// DeprecatedAttribute 表示过时标记零长度typeDeprecatedAttributestruct{MarkerAttribute// 嵌入标记属性的公共实现}// SyntheticAttribute 表示编译器生成标记零长度typeSyntheticAttributestruct{MarkerAttribute}// MarkerAttribute 标记属性的公共实现不读任何数据typeMarkerAttributestruct{}// readInfo 标记属性没有内容什么都不用读func(self*MarkerAttribute)readInfo(reader*ClassReader){// 什么也不做noting to do}readInfo是空的——因为标记属性没有内容要读。重点这里又用了 Go 的嵌入特性。MarkerAttribute提供空实现DeprecatedAttribute和SyntheticAttribute嵌入它自动获得readInfo()方法从而自动实现AttributeInfo接口。Deprecated过时标记对应 Java 里的Deprecated注解publicclassDemo{DeprecatedpublicvoidoldMethod(){}// ← 编译后有 Deprecated 属性}【Deprecated 的两种表示】 Java 源码Deprecated 注解 class 文件里同时存在两种表示 ① RuntimeVisibleAnnotations 属性 → 存注解信息供反射读取 ② Deprecated 属性 → 单纯的布尔标记供编译器/IDE 快速判断 为什么要有 ② → 因为 Deprecated 是个零长度的极简标记 → 判断这是否过时不需要解析复杂的注解结构 → 编译器、IDE 检查起来更快小知识Deprecated注解从 JDK 5 才有而Deprecated属性从 JDK 1.1 就有了。属性是老前辈注解是后来加的。Synthetic编译器生成标记表示这个成员类/字段/方法是编译器自动生成的源码里并不存在。典型的例子// 1. 内部类的 this$0 字段指向外部类实例的引用classOuter{classInner{}// Inner 会有个 synthetic 字段 this$0}// 2. 枚举的 values() 和 valueOf() 方法enumColor{RED,GREEN}// 编译器自动生成// public static Color[] values() ← synthetic// public static Color valueOf(String) ← synthetic// 3. 泛型桥接方法bridge methodclassMyListimplementsListString{publicStringget(intindex){...}// 编译器生成桥接方法Object get(int) ← synthetic bridge}// 4. 断言用到的 $assertionsDisabled 字段用javap -v可以看到static {}; descriptor: ()V flags: ACC_STATIC ... public static Color[] values(); descriptor: ()[LColor; flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNTHETIC ← 看这里重点ACC_SYNTHETIC标志位 Synthetic属性两者都能表示编译器生成。前者在 access_flags 里判断快后者是独立属性老式写法。现代编译器主要用标志位。四、javac 的 -g 参数与调试属性顺带讲个实用知识javac的-g参数控制生成哪些调试属性。# 生成全部调试信息默认javac-gHelloWorld.java# 不生成任何调试信息javac-g:noneHelloWorld.java# 只生成行号表和源文件javac -g:lines,source HelloWorld.java# 只生成局部变量表javac-g:varsHelloWorld.java参数生成的属性-g默认LineNumberTable LocalVariableTable SourceFile-g:none都不生成-g:linesLineNumberTable-g:varsLocalVariableTable-g:sourceSourceFile影响【有无调试信息的差异】 有调试信息-g Exception in thread main java.lang.NullPointerException at HelloWorld.main(HelloWorld.java:5) ↑ ↑ SourceFile LineNumberTable 无调试信息-g:none Exception in thread main java.lang.NullPointerException at HelloWorld.main(Unknown Source) ↑ 全都不知道了生产建议生产环境可以用-g:lines,source保留行号去掉变量表既能定位问题又能减小 class 文件体积。局部变量表vars对排查问题帮助不大但会显著增大文件。五、实战解析一个含常量的类写个测试类把 4 种属性都用上// AttrTest.javapublicclassAttrTest{publicstaticfinalintMAX_COUNT100;publicstaticfinaldoublePI3.14159;publicstaticfinalStringGREETINGHello;publicstaticintcounter0;DeprecatedpublicvoidoldMethod(){}}编译后解析javac-dD:\test D:\test\AttrTest.java ch03.exe-XjreC:\Java\jdk1.8.0_202\jre-cpD:\test AttrTest用javap -v -p看常量池和属性Constant pool: ... #20 Integer 100 #21 Double 3.14159d #22 String #23 // Hello #23 Utf8 Hello ... { public static final int MAX_COUNT; descriptor: I flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: int 100 ← ① ConstantValue 属性 public static final double PI; descriptor: D flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: double 3.14159d ← ① ConstantValue 属性 public static final java.lang.String GREETING; descriptor: Ljava/lang/String; flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL ConstantValue: String Hello ← ① ConstantValue 属性 public static int counter; descriptor: I flags: ACC_PUBLIC, ACC_STATIC ← ② 没有 ConstantValue 值在 clinit 里赋 public void oldMethod(); descriptor: ()V flags: ACC_PUBLIC Deprecated: true ← ③ Deprecated 属性 }再看看类的属性SourceFile: AttrTest.java ← ④ SourceFile 属性以及clinit()方法静态初始化static {}; descriptor: ()V flags: ACC_STATIC Code: stack1, locals0, args_size0 0: iconst_0 1: putstatic #2 // Field counter:I 4: return注意clinit里只有counter 0的赋值——因为三个static final常量的值已经在 ConstantValue 属性里了不需要在这里赋值。本篇小结本篇实现了 4 种简单属性ConstantValue结构constantvalue_index(u2)attribute_length固定为 2只有static final 基本类型/String 编译期常量才有值在类加载的「准备」阶段就赋好不需要执行代码重要推论访问static final常量不触发类初始化编译期内联SourceFile记录源文件名用于异常堆栈显示和调试器定位javac -g:none时不生成堆栈显示Unknown SourceDeprecated零长度属性length 0存在本身就是信息对应Deprecated注解但比注解更轻量Synthetic也是零长度属性标记编译器自动生成典型内部类的this$0、枚举的values()、泛型桥接方法Go 的嵌入复用MarkerAttribute提供空的readInfo()两个标记属性嵌入它自动实现接口下一篇进入整个属性表最核心的部分——Code 属性。方法的字节码就藏在里面它包含max_stack、max_locals、code[]字节码数组和异常表是我们实现解释器的直接输入。上一篇【第21篇】属性表概述——class 文件的百宝箱下一篇【第23篇】Code 属性——方法的灵魂

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价