02详解字节码文件
字节码文件(class 文件)是 JVM 跨平台、跨语言的基石。无论是 Java、Kotlin 还是 Scala,最终都会被编译成同一种字节码格式交给虚拟机执行。掌握字节码文件的结构,不仅能在面试中回答 int i = 0; i = i++; 这类问题,还能在工作中排查版本冲突、系统升级未生效等疑难杂症。本文按照「以正确的姿势打开文件 → 字节码文件的组成 → 玩转字节码常用工具」三条线索展开。
1.使用正确的软件打开字节码文件
字节码文件中保存了源代码编译之后的内容,以二进制的方式存储,无法直接用记事本打开阅读。通过 NotePad++ 安装十六进制插件后可以查看 class 文件原始的二进制内容,但这种方式非常不直观,只能看到一连串的字节,无法快速理解其中的含义。

推荐使用 jclasslib 工具查看字节码文件,它会把二进制内容解析成结构化的树形视图,让魔数、版本号、常量池、字段、方法等每一部分都一目了然。
- jclasslib GitHub 地址:https://github.com/ingokegel/jclasslib
- 也有 IDEA 插件版本,建议开发时使用插件版本,可以在代码编译之后实时看到字节码文件内容

2.字节码文件组成
字节码文件的组成分为五大部分:
- 基础信息:包括魔数、字节码文件对应的 Java 版本号、访问标识(public final 等)、父类和接口等。
- 常量池:保存了字符串常量、类或接口名、字段名,主要在字节码指令中使用。
- 字段:当前类或接口声明的字段信息。
- 方法:当前类或接口声明的方法信息以及字节码指令。
- 属性:类的属性,比如源码的文件名、内部类的列表等。

2.1 基本信息
Magic 魔数
Magic 魔数是文件开头的一段标识信息,让虚拟机识别到这个文件是 Java 的 class 文件,Java 的魔数固定为 0xCAFEBABE,不会改变。

其实不止 Java 的 class 文件有魔数。文件是无法通过文件扩展名来确定真实类型的——扩展名可以随意修改而不影响文件内容。软件一般使用文件的头几个字节(文件头)去校验文件类型,如果不支持该类型就会出错。常见的文件头如下:
| 文件类型 | 字节数 | 文件头 |
|---|---|---|
| JPEG (jpg) | 3 | FFD8FF |
| PNG (png) | 4 | 89504E47(文件尾也有要求) |
| bmp | 2 | 424D |
| XML (xml) | 5 | 3C3F786D6C |
| AVI (avi) | 4 | 41564920 |
| Java 字节码文件 (.class) | 4 | CAFEBABE |

主副版本号
主副版本号指的是编译字节码文件时使用的 JDK 版本号。
- 主版本号用来标识大版本号。JDK 1.0–1.1 使用了 45.0–45.3,JDK 1.2 是 46,之后每升级一个大版本号就加 1。JDK 1.2 之后大版本号计算方法就是:
主版本号 - 44,比如主版本号 52 对应的就是 JDK 8。 - 副版本号是当主版本号相同时作为区分不同版本的标识,一般只需要关心主版本号。

版本号的作用主要是判断当前字节码的版本和运行时的 JDK 是否兼容。如果字节码文件的主版本号高于运行时 JDK 的主版本号,虚拟机会抛出 UnsupportedClassVersionError。比如用 JDK 8(主版本号 52)编译的字节码文件,无法在 JDK 7(主版本号 51)上运行。
主版本号不兼容时通常有两种解决方法:
- 升级 JDK 版本(不推荐,因为有的依赖只有当前 JDK 版本才能使用,贸然升级 JDK 版本会导致其他依赖报错,并且需要大量的测试)。
- 降低依赖的版本号或者更换依赖,以满足 JDK 版本的要求(推荐采用)。
可以通过 jclasslib 看到字节码文件的基础信息组成,包括 Magic 魔数、主副版本号、访问标识以及类、父类、接口索引等,其中访问标识用于标识是类还是接口、注解、枚举、模块,以及 public final abstract 等修饰符;通过类、父类、接口索引可以找到对应类、父类、接口的信息。
| 名称 | 作用 |
|---|---|
| Magic 魔数 | 固定为 0xCAFEBABE,不会改变 |
| 副版本号 | 编译字节码文件的 JDK 版本 |
| 主版本号 | 编译字节码文件的 JDK 版本 |
| 访问标识 | 标识是类还是接口、注解、枚举、模块;标识 public final abstract |
| 类、父类、接口索引 | 通过这些索引可以找到类、父类、接口的信息 |
2.2 常量池
字节码文件中常量池的作用是:避免相同的内容重复定义,节省空间。例如源码中两次出现字符串 "我爱北京天安门",如果直接存储就会重复占用空间,常量池只保存一份,所有引用都指向同一处。
常量池中的数据都有一个编号,编号从 1 开始。在字段或者字节码指令中通过编号可以快速找到对应的数据。字节码指令中通过编号引用到常量池的过程称之为符号引用。

2.3 方法
字节码中的方法区域是存放字节码指令的核心位置,字节码指令的内容存放在方法的 Code 属性中。源代码经过编译后,方法体内的逻辑会被翻译成一条条字节码指令。

字节码指令在执行时会操作两个重要的数据结构:
- 操作数栈:临时存放数据的地方。
- 局部变量表数组:存放方法中的局部变量的位置,按数组下标访问,下标 0 通常存放
this(实例方法)或第一个参数,之后依次是方法中声明的局部变量。
以经典的面试题 int i = 0; i = i++; 为例,最终 i 的值仍然是 0。通过分析字节码指令可以发现:i++ 先把 0 取出来放入操作数栈中临时保存,接下来对局部变量表中的 i 进行加 1,i 变成了 1,最后再将之前保存的临时值 0 放回 i,最终 i 就变成了 0。
而 i = ++i; 的执行流程不同:先在局部变量表中对 i 加 1,再把 i 的值加载到操作数栈,最后保存回 i,最终 i 的值是 1。这也是 i++ 与 ++i 在字节码层面的本质差别。

3.分析字节码常用工具
3.1 javap -v 命令
javap 是 JDK 自带的反编译工具,可以通过控制台查看字节码文件的内容,适合在服务器上使用。
- 直接输入
javap可以查看所有参数说明。 - 输入
javap -v 字节码文件名称可以查看具体的字节码信息。如果是一个 jar 包,需要先使用jar -xvf命令解压,再对解压出来的 class 文件执行 javap。

这个工具比较简陋,正常本地调试可以使用 jclasslib 插件,线上环境则可以配合 arthas 使用。
3.2 arthas
Arthas 是阿里巴巴出品的一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,大大提升线上问题排查效率。
- 官网:https://arthas.aliyun.com/doc/
- 主要功能涵盖方法监控、类的热部署、内存监控、垃圾回收监控、应用热点定位、监控面板、查看字节码信息等。
与字节码相关的常用命令有:
dump 类的全限定名:dump 已加载类的字节码文件到特定目录,方便拿回本地用 jclasslib 进一步分析。jad 类的全限定名:反编译已加载类的源码。

一个典型的使用场景:小李的团队对系统进行了升级修复了某个 bug,但是升级完之后发现 bug 还存在。怀疑是因为没有把最新的字节码文件部署到服务器上,这时可以在出问题的服务器上启动 arthas,使用 jad 命令加上想要查看的类名,反编译出当前 JVM 中实际运行的源码,确认源码是否是最新的,从而判断部署是否成功。
总结一下:本地文件可以使用 jclasslib 工具查看,开发环境使用 jclasslib 插件;服务器上的文件可以使用 javap 命令直接查看,也可以通过 arthas 的 dump 命令导出字节码文件再查看本地文件,或者使用 jad 命令直接反编译出源代码。