阅读本文你能收获到
- 了解class字节码文件的结构
- 熟悉 jvm 时如何读取解析字节码
tip : 本文请结合 Java字节码内容的那些表 和 Java字节码的那些指令 进行学习, 使用到16进制转UTF-8字符串工具。
任何一个Class文件都对应着一个类或者接口的定义信息。但是类和接口也可以通过类加载其直接生成,所以不一定定义在文件里面。
Class文件是一组以8字节为基础的单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间不会添加任何分隔符。
下面以简单例子分析
1 | //TestClass.java 文件 |
使用 javac
编译 TestClass.java
输出 TestClass.Class
,得到的二进制流文件可以通过工具查看其内容。笔者在 MAC 平台上使用 iHex-Hex Editor
以十六进制格式查看。
1 | CAFEBABE 00000034 00130A00 04000F09 00030010 07001107 00120100 016D0100 01490100 063C696E |
上述的内容看起来像 “天书” 无从下手, 这时候就要用到 Java字节码内容的那些表。JVM为了能统一解析这些 “天书”, 要求其内容必须按照严格的格式来排版。查看 Class文件结构表 可知上述内容有规律可循,为了增强上述内容的可读性,我按照 Class 文件结构重新排序了内容并标注了行数,方便后续分析。
1 | line 1 : CAFEBABE |
以下解析流程涉及的外链比较多, 在解析过程中需要跳转查阅对应内容。基本的逻辑都某个索引指向某个数据结构, 该数据结构对应一张表格的内容,于是向下解析该表格内容长度的内容。
line 1 为 魔数
, 主要是用于确认这个文件是否能被虚拟机加载, CAFEBABE 其实就是 Baristas咖啡
。
line 2 为 主次版本号
, 从 java-class版本对应 可知表示 Java SE 10, 向下兼容到 JDK1.1。
line 3 为 常量池中常量数量
, 由于从1开始计数,第0项预留用于表示“不引用任何一个常量池项目”, 转化 16 进制之后可知常量池有 18 项常量。 每一项常量都对应 常量表 的某一项, 按照表中规定的每一项常量对应各自的结构。
line 4 为 第 1 项常量
,为类中方法的符号引用, 格式为 {第4项常量}.{第15项常量} ,为 “java/lang/Object.< init >:()V”
line 5 为 第 2 项常量
,为字段的符号引用, 格式为 {第3项常量}.{第16项常量} , 为 “TestClass.m:I”
line 6 为 第 3 项常量
,为类或接口的符号引用, 指向第 17 项常量,为 “TestClass”
line 7 为 第 4 项常量
,为类或接口的符号引用, 指向第 18 项常量,为 “java/lang/Object”
line 8 为 第 5 项常量
,为 UTF-8 编码的字符串, 长度为1, 转化得到 “m”
line 9 为 第 6 项常量
,为 UTF-8 编码的字符串, 长度为1, 转化得到 “I”
line 10 为 第 7 项常量
,为 UTF-8 编码的字符串, 长度为6, 转化得到 “< init >”
line 11 为 第 8 项常量
,为 UTF-8 编码的字符串, 长度为3, 转化得到 “()V”
line 12 为 第 9 项常量
,为 UTF-8 编码的字符串, 长度为4, 转化得到 “Code”
line 13 为 第 10 项常量
,为 UTF-8 编码的字符串, 长度为15, 转化得到 “LineNumberTable”
line 14 为 第 11 项常量
,为 UTF-8 编码的字符串, 长度为3, 转化得到 “inc”
line 15 为 第 12 项常量
,为 UTF-8 编码的字符串, 长度为3, 转化得到 “()I”
line 16 为 第 13 项常量
,为 UTF-8 编码的字符串, 长度为14, 转化得到 “SourceFile”
line 17 为 第 14 项常量
,为 UTF-8 编码的字符串, 长度为10, 转化得到 “TestClass.java”
line 18 为 第 15 项常量
,为字段或方法的部分符号引用, 格式为 {第7项常量}:{第8项常量} “< init >:()V”
line 19 为 第 16 项常量
,为字段或方法的部分符号引用,格式为 {第5项常量}:{第6项常量}, 为 “m:I”
line 20 为 第 17 项常量
,为 UTF-8 编码的字符串, 长度为9, 转化得到 “TestClass”
line 21 为 第 18 项常量
,为 UTF-8 编码的字符串, 长度为16, 转化得到 “java/lang/Object”
line 22 为 访问标志
, 查看 类访问标志 可知为 0x0021(0x0001|0x0020)表明这个是一个普通类,既不是接口,枚举也不是注解,被public关键字修饰但没有被声明为final和abstract
line 23 为 类索引
, 对应 第 3 项常量
, 为 “TestClass”
line 24 为 父类索引
, 对应 第 4 项常量
, 为 “java/lang/Object”
line 25 为 实现接口的数目
, 0 表示没有实现任何接口
line 26 为 字段的数目
, 存在一个字段需要解析
line 27 为 第 1 个字段
, 查看 字段表 可知 访问标志(0002)为 private, 名字(0005)为 m, 描述(0006)为 I, 没有属性(0000)
line 28 为 方法的数目
, 存在两个方法需要解析
line 29-32 为 第 1 个方法
, 查看 方法表 可知 访问标志(0001)为 public, 名字(0007)为 第 9 项常量
Code, 查看 属性表-Code属性结构及结合Java字节码的那些指令 继续可知。
以下为Code属性所有内容
- 0009, 起始标示 “code”
- 0000001D, 该属性的长度为 29( 2+2+4+5+2+2+2+4+2+2+2), 也就是该属性后面的所有内容长度之和
- 0001, 操作数栈深度的最大值,也就是说方法执行的任意时刻操作栈不会超过深度 1
- 0001, 局部变量表需要的存储空间,单位 Slot,这个使用 1 个 Slot
- 00000005, 字节码指令长度为 5
- 2A B7 0001 B1, 代表 “ aload_0 invokespecail java/lang/Object.”
“:()V return” - 0000, 需要处理的异常为 0个
- 0001, 有一个属性
- 000A, 起始标志为 “LineNumberTable” ,用于描述Java源代码行号与字节码行号的对应关系,可查看 LineNumberTable
- 00000006, 该属性的长度为 6
- 0001 lineNumberInfo 长度为 1
- 0000, 字节码行号 0
- 0001, Java源代码行号 1
line 33-36 为 第 2 个方法
, 查看 方法表 可知 访问标志(0001)为 public, 名字(000B)为 inc, 描述(000C)为 ()I, 一个属性(0001)。也存在一个属性。从 34 行开始解析属性 0009解析为 第 9 项常量
Code, 继续看该 Code 的值。
以下为Code属性所有内容
- 0009, 起始标示 “code”
- 0000001F, 该属性的长度为 31(2+2+4+7+2+2+2+4+2+2+2)
- 0002, 操作数栈深度的最大值,也就是说方法执行的任意时刻操作栈不会超过深度 2
- 0001, 局部变量表需要的存储空间,单位 Slot,这个使用 1 个 Slot
- 00000007, 字节码指令长度为 7
- 2A B4 0002 04 60 AC, 代表 “aload_0 getfield TestClass.m:I iconst_1 iadd ireturn”
- 0000, 需要处理的异常为 0个
- 0001, 有一个属性
- 000A, 起始标志为 “LineNumberTable” ,用于描述Java源代码行号与字节码行号的对应关系,可查看 LineNumberTable
- 00000006, 该属性的长度为 6
- 0001 lineNumberInfo 长度为 1
- 0000, 字节码行号 0
- 0001, Java源代码行号 6
line 37 为 属性的数目
, 存在一个属性
line 38 为 为 第 1 个属性
, 000D解析为 第 13 项常量
“sourceFile”, 解析 sourceFile属性得到 “ TestClass.java”
为了验证我们的思路是否正确,可以通过 javap
查看 TestClass.class
的结构来进行对比。
1 | gih-d-14238:java yummy$ javap -verbose TestClass |
实际上 javap
也是按照我们分析字节码的逻辑来进行内容的输出, 感兴趣的伙伴可以查看 javap
内部实现。