源码之前,了无秘密。——《编程珠玑》
作为 Java 开发者,我们每天编写 .java 文件,却很少关心编译后的 .class 文件里到底藏了什么。本文将带你从 Hex 层面逐字节拆解 Class 文件,彻底搞懂 JVM 的类加载基石。
一、Class 文件概览
Class 文件是 Java 虚拟机执行引擎的"食材",它是一组以 8 位字节为基础单位的二进制流,各数据项严格按照顺序紧凑排列,没有任何分隔符。
1.1 整体结构
| 数据类型 | 名称 | 数量 |
|---|---|---|
| u4 | magic(魔数) | 1 |
| u2 | minor_version(次版本号) | 1 |
| u2 | major_version(主版本号) | 1 |
| u2 | constant_pool_count(常量池容量) | 1 |
| cp_info | constant_pool(常量池) | constant_pool_count - 1 |
| u2 | access_flags(访问标志) | 1 |
| u2 | this_class(类索引) | 1 |
| u2 | super_class(父类索引) | 1 |
| u2 | interfaces_count(接口数) | 1 |
| u2 | interfaces(接口索引集合) | interfaces_count |
| u2 | fields_count(字段数) | 1 |
| field_info | fields(字段表) | fields_count |
| u2 | methods_count(方法数) | 1 |
| method_info | methods(方法表) | methods_count |
| u2 | attributes_count(属性数) | 1 |
| attribute_info | attributes(属性表) | attributes_count |
💡
u1、u2、u4分别代表 1、2、4 个字节的无符号数
二、Hex 实战:写一个最简单的类
先创建一个最基础的 Java 类:
// Hello.java
public class Hello {
private static final int MAGIC = 0xCAFEBABE;
public static void main(String[] args) {
System.out.println("Hello, Class File!");
}
}
编译后,用 xxd 或 Hex 编辑器打开 Hello.class:
javac Hello.java
xxd Hello.class | head -20
输出:
00000000: cafe babe 0000 0034 0013 0a00 0400 0f09 .......4........
00000010: 0003 0010 0700 1107 0012 0100 054d 4147 .............MAG
00000020: 4943 0100 0149 0100 0d43 6f6e 7374 616e IC...I...Constan
00000030: 7456 616c 7565 0300 0000 0001 0006 3c69 tValue.......<i
三、逐字节解析
3.1 魔数(Magic Number)- 4 bytes
cafe babe
- 位置:第 0-3 字节
- 值:
0xCAFEBABE(咖啡宝贝 ☕) - 作用:标识这是一个合法的 Class 文件
很多文件格式都有魔数:JPEG 是
FF D8 FF,PNG 是89 50 4E 47
3.2 版本号 - 4 bytes
0000 0034
- 次版本号(minor_version):
0x0000→ 0 - 主版本号(major_version):
0x0034→ 52
版本对照表:
| JDK 版本 | 十进制 | 十六进制 |
|---|---|---|
| JDK 1.1 | 45 | 0x2D |
| JDK 1.2 | 46 | 0x2E |
| ... | ... | ... |
| JDK 8 | 52 | 0x34 |
| JDK 11 | 55 | 0x37 |
| JDK 17 | 61 | 0x3D |
| JDK 21 | 65 | 0x41 |
所以 0x34 = JDK 8 编译的类文件。
3.3 常量池(Constant Pool)
0013
- constant_pool_count:
0x0013= 19 - 实际常量数:19 - 1 = 18 个(索引 0 保留)
常量池是 Class 文件的 "资源仓库",存放:
- 字面量(字符串、final 常量值)
- 符号引用(类名、方法名、字段名)
每个常量以 tag(1 byte)开头,标识类型:
| Tag | 类型 | 说明 |
|---|---|---|
| 1 | CONSTANT_Utf8 | UTF-8 字符串 |
| 3 | CONSTANT_Integer | 整型字面量 |
| 4 | CONSTANT_Float | 浮点字面量 |
| 7 | CONSTANT_Class | 类/接口符号引用 |
| 8 | CONSTANT_String | 字符串字面量 |
| 9 | CONSTANT_Fieldref | 字段符号引用 |
| 10 | CONSTANT_Methodref | 方法符号引用 |
| 12 | CONSTANT_NameAndType | 名称和类型 |
3.4 访问标志(Access Flags)- 2 bytes
0021
0x0021 = 0x0001 | 0x0020 = ACC_PUBLIC | ACC_SUPER
常见标志位:
| 标志名 | 值 | 含义 |
|---|---|---|
| ACC_PUBLIC | 0x0001 | public |
| ACC_FINAL | 0x0010 | final |
| ACC_SUPER | 0x0020 | 允许 invokespecial 字节码指令 |
| ACC_INTERFACE | 0x0200 | 接口 |
| ACC_ABSTRACT | 0x0400 | 抽象类 |
3.5 类索引与父类索引
0003 // this_class → 指向常量池 #3
0004 // super_class → 指向常量池 #4(java/lang/Object)
3.6 字段表与方法表
字段表描述类中声明的变量(实例变量 + 类变量),方法表描述方法定义。两者结构类似:
// 字段表结构
field_info {
u2 access_flags; // 访问标志
u2 name_index; // 名称索引(指向常量池)
u2 descriptor_index; // 描述符索引
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count];
}
描述符规则:
| 标识字符 | 含义 |
|---|---|
| B | byte |
| C | char |
| D | double |
| F | float |
| I | int |
| J | long |
| L | 对象类型(如 Ljava/lang/Object;) |
| S | short |
| Z | boolean |
| [ | 数组(如 [I 表示 int[]) |
| V | void |
四、实用工具推荐
4.1 javap - 官方反编译器
# 查看基本信息
javap -v Hello.class
# 查看字节码
javap -c Hello.class
-v 参数输出的详细信息,其实就是对 Class 文件结构的"人类可读版"翻译。
4.2 jclasslib 插件
IDEA 插件,可视化查看 Class 文件结构,比 Hex 编辑器友好太多:
Settings → Plugins → 搜索 "jclasslib" → 安装
4.3 Hex Fiend / 010 Editor
专业 Hex 编辑器,配合 Class 文件模板,解析一目了然。
五、总结
| 知识点 | 要点 |
|---|---|
| 魔数 | 0xCAFEBABE,Class 文件身份证 |
| 版本号 | 主版本号 52 = JDK 8 |
| 常量池 | 资源仓库,从索引 1 开始 |
| 访问标志 | 按位或运算组合 |
| 描述符 | 字段/方法的类型签名 |
理解 Class 文件结构,是深入 JVM 的必经之路。无论是排查类加载问题、分析字节码增强,还是手写 Class 文件,这些知识都将成为你的利器。
📌 文末思考:你能用 Hex 编辑器手动修改 Class 文件的版本号,让它在低版本 JDK 上运行吗?有什么风险?欢迎在评论区讨论!