深入理解Class文件结构
概述
我们都知道编写的Java的源码会先编译成Class文件,java虚拟机再将Class文件解释编译成对应平台的机器指令,所以能够解析Class文件的数据结构是非常有必要的。
先编写一段java源码,定义一个类并实现一个接口,类内部定义了一个成员变量、一个类变量和一个sum方法。
package com.changyy.jvm;
public class ClassTest implements IClassTest {
private int n = 10;
private static int m = 5;
public int sum(int p) {
return n + p;
}
}
package com.changyy.jvm;
public interface IClassTest {
}
查看编译后的Class文件,可以使用notepad++的HEX-Editor插件、IDEA的BinEd插件等工具。
Class文件只有两种数据类型:“无符号数”和“表”
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表:是由多个无符号数或者其他作为数据项构成的复合数据类型,表一般以“_info”结尾。
Class文件结构表
类型 | 名称 | 说明 | 长度 |
---|---|---|---|
u4 | magic | 魔数,识别Class文件格式 | 4个字节 |
u2 | minor_version | 次版本号 | 2个字节 |
u2 | major_version | 主版本号 | 2个字节 |
u2 | constant_pool_count | 常量池容量计数值 | 2个字节 |
cp_info | constant_pool | 常量池 | n个字节 |
u2 | access_flags | 访问标志 | 2个字节 |
u2 | this_class | 类索引 | 2个字节 |
u2 | super_class | 父类索引 | 2个字节 |
u2 | interfaces_count | 接口计数器 | 2个字节 |
u2 | interfaces | 接口索引集合 | 2个字节 |
u2 | fields_count | 字段个数 | 2个字节 |
field_info | fields | 字段集合 | n个字节 |
u2 | methods_count | 方法计数器 | 2个字节 |
method_info | methods | 方法集合 | n个字节 |
u2 | attributes_count | 属性计数器 | 2个字节 |
attribute_info | attributes | 属性集合 | n个字节 |
接下来开始分析字节码
魔数
Class文件的前4个字节称为魔数(0xCAFEBABE、咖啡宝贝),作用是确定这个文件是否能被java虚拟机所接受的Class文件.
版本号
按照Class文件结构表接下来是两个字节的次版本号和两个字节的主版本号,次版本号0x0000 -> 0,主版本号 0x0031 ->52(十六进制转十进制),版本号52.0也就是对应jdk1.8。
[图片上传失败...(image-ae6eaa-1604406038568)]
常量池
接下来是常量池容器计数值,两个字节0x001d -> 29 ,代表常量池中有28项常量,索引值范围位1~28。为什么不是从0开始,是因为它把第0项常量空出来了。这是为了在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。
[图片上传失败...(image-ea57e1-1604406038568)]
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。
-
符号引用总结起来则包括了下面三类常量:
被模块导出或者开放的包
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
方法句柄和方法类型
动态调用点和动态常量
全限定名:com/changyy/jvm/ClassTest就是类的全限定名,
字段方法的名称:Java代码中的sum和n、m就是方法、字段的简单名称
字段的描述符:
[图片上传失败...(image-19aefc-1604406038568)]
方法的描述符:
[图片上传失败...(image-f4a192-1604406038568)]
常量池的项目类型
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MothodType_info | 16 | 标志方法类型 |
CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
通过上图接下来逐一解析常量池,共28项常量
[图片上传失败...(image-c7f9ca-1604406038568)]
读取下一个字节标志位tag用于区分常量类型 0x0a->10,在类型查找标志是10的,可知这个常量属于CONSTANT_Methodref_info类型,此类型的常量代表类中方法的符号引用,CONSTANT_Methodref_info的结构如下:
[图片上传失败...(image-f236ad-1604406038568)]
tag占一个字节、class_index占两个字节、name_and_type占两个字节,class_index:0x0005 -> 5 指向常量池的第五项常量,name_and_type_index:0x0017->23 指向常量池的第二十三项常量。
[图片上传失败...(image-e4cd15-1604406038568)]
使用JDK的一个工具 javap来输出ClassTest.class的字节码内容,可以用来验证解析字节码是否正确。
javap -v ClassTest.class
其中的常量池部分
Constant pool:
#1 = Methodref #5.#23 // java/lang/Object."":()V
#2 = Fieldref #4.#24 // com/changyy/jvm/ClassTest.n:I
#3 = Fieldref #4.#25 // com/changyy/jvm/ClassTest.m:I
#4 = Class #26 // com/changyy/jvm/ClassTest
#5 = Class #27 // java/lang/Object
#6 = Class #28 // com/changyy/jvm/IClassTest
#7 = Utf8 n
#8 = Utf8 I
#9 = Utf8 m
#10 = Utf8
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/changyy/jvm/ClassTest;
#17 = Utf8 sum
#18 = Utf8 (I)I
#19 = Utf8 p
#20 = Utf8
#21 = Utf8 SourceFile
#22 = Utf8 ClassTest.java
#23 = NameAndType #10:#11 // "":()V
#24 = NameAndType #7:#8 // n:I
#25 = NameAndType #9:#8 // m:I
#26 = Utf8 com/changyy/jvm/ClassTest
#27 = Utf8 java/lang/Object
#28 = Utf8 com/changyy/jvm/IClassTest
从这里可以看出第一项常量确实是CONSTANT_Methodref_info,索引值也都对应。
接着解析,下1个字节是tag: 0x09 -> 9 类型是CONSTANT_Fieldref_info的常量,表示字段的符号引用,CONSTANT_Fieldref_info的结构如下:
[图片上传失败...(image-d918c-1604406038568)]
tag占一个字节、class_index占两个字节、name_and_type占两个字节,class_index:0x0004 -> 4 指向常量池的第四项常量,name_and_type_index:0x0018->24 指向常量池的第二十四项常量,可以通过javap出的常量池进行验证。
[图片上传失败...(image-70ea85-1604406038568)]
继续1个字节是 tag:0x09 -> 9 还是CONSTANT_Fieldref_info类型的常量class_index:0x0004 -> 4 指向常量池的第四项常量,name_and_type_index:0x0019->25 指向常量池的第二十五项常量,
[图片上传失败...(image-d35e19-1604406038568)]
继续1个字节是tag:0x07 -> 7 类型是CONSTANT_Class_info的常量,表示类或接口的符号引用。结构如下:
[图片上传失败...(image-bd3c68-1604406038568)]
共占3个字节,tag:0x07,name_index:0x001a -> 26 指向常量池中第二十六项常量
[图片上传失败...(image-96a41f-1604406038568)]
继续一个字节 tag:0x07 -> 7还是CONSTANT_Class_info类型常量,name_index:0x001b -> 27 指向常量池中第二十七项常量
[图片上传失败...(image-7fa154-1604406038568)]
继续一个字节 tag:0x07 -> 7还是CONSTANT_Class_info类型常量,name_index:0x001c -> 28 指向常量池中第二十八项常量
[图片上传失败...(image-510d8b-1604406038568)]
继续一个字节 tag:0x01 -> 1 类型是CONSTANT_Utf8_info的常量,表示UTF-8编码的字符串,结构如下:
[图片上传失败...(image-80fd5d-1604406038568)]
共占4个字节,length:0x0001 -> 1 bytes:6e转成utf-8是 n
[图片上传失败...(image-5d21c9-1604406038568)]
由于常量池项太多,不一一解析了,可以参考文末的思维导图一步步解析
访问标志
access_flags用两个字节来表示,其标识了类或者接口的访问信息,结构如下:
0x0001|0x0020=0x0021 是一个public类型的类
[图片上传失败...(image-d1c06d-1604406038568)]
类索引、父类索引
类索引(this_class)和父类索引(super_class)都是两个字节的数据
0x0004 -> 4 指向常量池中类型为CONSTANT_Class_info的类描述符 第四项常量
0x0005 -> 5 指向常量池中类型为CONSTANT_Class_info的类描述符 第五项常量
[图片上传失败...(image-e2f9b3-1604406038568)]
接口索引集合
接口计数器:两个字节,0x0001 - > 1 表示实现了一个接口
[图片上传失败...(image-2aba3c-1604406038568)]
接口集合:接口数量是1个,那就读取两个字节,指向常量池中类型为CONSTANT_CLass_info的接口描述符。 0x0006 -> 6 常量池中第6项常量
[图片上传失败...(image-8c2938-1604406038568)]
字段表集合
首先是fields_count字段数量,占两个字节,0x0002 -> 2 表示有两个字段
[图片上传失败...(image-85d82c-1604406038568)]
字段表结构如下
[图片上传失败...(image-3f41b8-1604406038568)]
字段修饰符access_flag 0x0002 -> 2,表示字段的访问标志是private
name_index:0x0007 -> 7 指向常量池类型为CONSTANT_Utf8_info的字段简单名称,常量池中第7项 也就是变量 n
descriptor_index:0x0008 -> 指向常量池类型为CONSTANT_Utf8_info的字段描述符,常量池中第8项 也就是变量 I
[图片上传失败...(image-a680fa-1604406038568)]
attributes_count:0x0000 -> 0 表示没有属性
接下来第二个字段 access_flag:0x000a == 0x0002 | 0x0008 访问修饰符是private + static
[图片上传失败...(image-3310d4-1604406038568)]
name_index:0x0009 -> 9 指向常量池类型为CONSTANT_Utf8_info的字段简单名称,常量池中第9项 也就是变量 m
descriptor_index:0x0008 -> 指向常量池类型为CONSTANT_Utf8_info的字段描述符,常量池中第8项 也就是变量 I
attributes_count:0x0000 -> 0 表示没有属性
方法表集合
首先是两个字节表示方法数量 0x0003 -> 3 表示3个方法
[图片上传失败...(image-e4cde6-1604406038568)]
方法表methods结构如下:
[图片上传失败...(image-8833c6-1604406038568)]
第一个方法 [图片上传失败...(image-5dcb2-1604406038568)]
access_flags:0x0001 表示方法的访问修饰符是public
name_index:0x000a ->10 指向常量池类型为CONSTANT_Utf8_info的方法的简单名称,常量池中第10项常量
descriptor_index:0x000b -> 11 指向常量池类型为CONSTANT_Utf8_info的方法的描述符,常量池中第11项常量 ()V,方法描述符结构如下:
[图片上传失败...(image-b11fea-1604406038568)]
attributes_count 0x0001 -> 1 有一个属性
attributes属性集合,attribute_name_index:0x000c -> 12 指向常量池中第12项常量,Code属性
[图片上传失败...(image-78b264-1604406038568)]
Code属性结构如下:
attribute_length:4个字节 0x00000039 -> 57 ,由于属性名称索引和属性长度一共6个字节,所以属性值的长度是整个属性表的长度减去6个字节
max_statck:0x0002 ->2 表示操作数栈深度最大值是2
max_locals:0x0001 ->1 表示局部变量表所需1个Slot
code_length:0x0000000b -> 11 表示字节码的长度是11
[图片上传失败...(image-7e4695-1604406038568)]
接下来是code 读取11个字节
字节码指令可以去oracle官网 https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html 查找,0x2a表示aload_0,这部分可以通过javap反编译查看
public com.changyy.jvm.ClassTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field n:I
10: return
LineNumberTable:
line 8: 0
line 10: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/changyy/jvm/ClassTest;
接下来是exception_table_length 异常表长度 0x0000 -> 0
[图片上传失败...(image-f64cbb-1604406038568)]
attributes_count:0x0002 -> 2 2个属性
[图片上传失败...(image-632053-1604406038568)]
第一个属性attribute_name_index:0x000d ->13 指向常量池中第13项常量 LineNumberTable
LineNumberTable属性:描述Java源代码与字节码行号(字节码的偏移量)之间的对应关系,结构如下:
[图片上传失败...(image-cf9789-1604406038568)]
attribute_length:0x0000000a -> 10
line_number_table_length:0x0002 -> 2 长度为2
[图片上传失败...(image-119b9b-1604406038568)]
第一个line_number_info, start_pc 0x0000 -> 0 ,line_number 0x0008 -> 8 字节码第0行对应Java源代码第8行
第二个line_number_info, start_pc 0x0004 ->4,line_number 0x000a -> 10 字节码第4行对应Java源代码第10行
[图片上传失败...(image-2c17a9-1604406038568)]
第一个属性attribute_name_index:0x000e ->14 指向常量池中第14项常量 LocalVariableTable
[图片上传失败...(image-549f52-1604406038568)]
LocalVariableTable属性:用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,结构如下:
[图片上传失败...(image-b73e4f-1604406038568)]
attribute_lenght:0x000000c -> 12 覆盖长度12,
local_variable_table_length : 0x0001 -> 1 表示一个local_variable_table
[图片上传失败...(image-959926-1604406038568)]
start_pc: 0x0000 ->0 ; length: 0x000b -> 11; name_index: 0x000f ->15;descriptor_index: 0x0010->16;index: 0x0000 ->0;解释如上图。
后面二个方法就不再解析了,可以通过后面思维导图自行解析
关注微信公众号“程序员二胖” 回复 002 获取Class文件结构思维导图原图。