适用读者:想搞懂"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. 自检
.class的顶层结构有哪几段?invokestatic #7里的#7是什么?为什么叫"符号引用"?- 常量池的两个作用?
- 描述符
(I)Ljava/lang/Integer;表示什么? - 为什么反编译能还原结构、但看不到注释?
8. 下一篇
S04-J04 · 泛型:擦除、通配符与它的代价
字节码讲完,下一篇解释"为什么 List<String> 运行时不知道自己装的是 String",并用反射实测绕过泛型检查。
本篇属于系列《S04 · Java 语言核心》。旧文来源:A-2587;扩展点 E-S04-014。javap 输出为 OpenJDK 21 实测;类文件结构细节为规范口径。