1165 字
约 3 分钟
6
S04-J03 · 类文件结构与字节码

适用读者:想搞懂"JVM 看到的代码长什么样"的人。
前置:S04-B01。
实测环境:OpenJDK 21.0.12.1(javap,2026-11-05)。
覆盖旧文:A-2587(Java 类文件结构,662 点)。
覆盖扩展:E-S04-014(常量池与符号引用)。
上一篇:S04-J02 · HashMap 源码:哈希、扩容、树化与线程不安全
下一篇:S04-J04 · 泛型擦除与通配符。


1. 一句话结论

.class 不是"编译后的机器码",而是一份结构化的描述文件**:常量池 + 访问标志 + 字段表 + 方法表 + 属性表。JVM 靠常量池里的"符号引用"在运行时解析出真正的地址。**

magic(0xCAFEBABE) | minor | major | constant_pool_count | constant_pool
| access_flags | this_class | super_class | interfaces
| fields | methods | attributes

2. 实测:javap -c 看到什么

$ javap -c Box | head -20

实测输出(节选):

public static void main(java.lang.String[]);
    Code:
       0: bipush        127
       2: invokestatic  #7    // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: bipush        127
       8: invokestatic  #7
      12: sipush        128
      15: invokestatic  #7
元素 含义
bipush 127 / sipush 128 常量入栈(单字节 vs 双字节指令)
invokestatic #7 调用静态方法,#7 是常量池索引
// Method java/lang/... javap 帮你解析出的符号引用
astore_1 存入局部变量表槽 1

关键点:#7 这样的索引就是"符号引用"——编译时不知道 Integer.valueOf 在内存哪里,只记名字;JVM 在首次解析时才转成直接引用(这是"动态链接"的基础)。


3. 常量池(E-S04-014)

常量类型 内容
Utf8 字符串字面量、类名、方法名
Integer/Float/Long/Double 数值常量
Class 类引用
Fieldref/Methodref/InterfaceMethodref 字段/方法符号引用
String 字符串常量(指向 Utf8)

常量池的两个作用:

作用 说明
去重 相同字符串/名字只存一份
符号引用 编译期不绑定地址,运行期解析

这就是为什么改一个类的方法名、但没重新编译调用方,会报 NoSuchMethodError——调用方的常量池里还记着旧名字。


4. 方法表与栈帧

结构 内容
方法表项 访问标志、名字、描述符((I)Ljava/lang/Integer;)、Code 属性
栈帧 局部变量表 + 操作数栈 + 动态链接 + 返回地址

描述符的读法:

(I)Ljava/lang/Integer;   →  参数 int,返回 Integer
([Ljava/lang/String;)V   →  参数 String[],返回 void

判据:能读描述符,就能在没有源码时判断方法签名(反编译/排障常用)。


5. 为什么能被反编译

字节码保留了足够多的结构信息(类名、方法名、字段名、行号表),所以反编译能把结构还原得八九不离十。

信息 保留 结果
类/方法/字段名 ✅ 可还原
行号表 ✅(调试信息) 可映射到源码行
局部变量名 ⚠️(需 -g) 可能变成 var1
泛型信息 ✅(Signature 属性) 反射能读到(J04)
注释 ❌ 永久丢失

这解释了"混淆"的原理:把名字改成 a/b/c,结构还在,但可读性大幅下降。


6. 五个误区

误区 真相
".class 是机器码" 是 JVM 指令集 + 结构化元数据
"方法调用编译期就绑定地址" 记录符号引用,运行期解析
"常量池只存字符串" 还有类/字段/方法引用与数值
"反编译能看到注释" 注释不保留
"改了类名不重新编译没事" 调用方常量池仍是旧名字 → 运行期报错

7. 自检

  1. .class 的顶层结构有哪几段?
  2. invokestatic #7 里的 #7 是什么?为什么叫"符号引用"?
  3. 常量池的两个作用?
  4. 描述符 (I)Ljava/lang/Integer; 表示什么?
  5. 为什么反编译能还原结构、但看不到注释?

8. 下一篇

S04-J04 · 泛型:擦除、通配符与它的代价

字节码讲完,下一篇解释"为什么 List<String> 运行时不知道自己装的是 String",并用反射实测绕过泛型检查。


本篇属于系列《S04 · Java 语言核心》。旧文来源:A-2587;扩展点 E-S04-014。javap 输出为 OpenJDK 21 实测;类文件结构细节为规范口径。

S04-J03 · 类文件结构与字节码
https://www.clxhxhhr.top/posts/5226/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。