基础概念
- 任何一个 Class 文件都对应着唯一的一个类或接口的信息
- Class 文件是一组以字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。这使得整个 Class 文件中存储的内容几乎全部是 程序运行的必要数据
- Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构只有“无符号数” 和 “表”两种数据类型
无符号数
无符号数属于基本的数据类型,以 u1,u2,u4,u8 来分别代表 1 个字节、2 个字节、4 个字节、8 个字节
表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表的命名都习惯性地以“_info” 结尾
对上面代码进行编译得到 .class 文件
组成部分
一、魔数
每个Class文件的头4 个字节被称为魔数,它的唯一作用是确定这个文件是否是一个能被虚拟机接受的 Class 文件
二、版本号
紧接着魔数的 4 个字节存储的是 Class 文件的版本号
- 第5个和第6个字节是次版本号:次版本号未使用,全部固定为 0
- 第7个和第8个字节是主版本号:Java的版本号是从 45 开始的,JDK1.1之后每个JDK 大版本的发布,主版本号向上加 1,0x003B 对应的十进制为 59(我使用的JDK 15, 所以是主版本号是59)
三、常量池
- 紧接着主次版本之后的是常量池入口,常量池可以比喻为 Class 文件里的资源仓库,它是 Class 文件结构中与其它项目关联最多的数据。通常也是占用 Class 文件空间最大的数据项目之一
- 常量池的入口需要放置一项 u2 类型的数据,表示常量池容量计数值,这个容量计数是从 1 开始,而不是从 0 开始
0x0016 转化为10进制为22, 表示常量池中有 21 个常量,索引值的范围为 1~22
-
常量池中主要存放两大类变量:字面量和符号引用
-
常量池中每一个常量都是一张表,截至 JDK13,常量表中一共有 17 种不同类型的常量
例子分析:第一项常量
第一项常量的标志位为 0x0A, 对应十进制 10,则表示这个常量属于 CONSTANT_Methodref_info 类型,此类型常量代表类中方法的符号引用,CONSTANT_Methodref_info 的结构如下图所示:一共占5个字节
tag 是标志位,用于区分常量类型, index 是索引值,图中可以参数 第二个index 为 0x02 ,指向常量池中的第二项常量,继续查找第二项常量:为 0x03 ,查表可知这是一个 CONSTANT_Class_info 类型的常量,表示类或者接口的符号引用
例子分析:第二项常量
到此为止,分析了TestClass.Test 常量池中 21 个常量的两个还未提到的 19 个常量都可以用类似的方法分析出来,可以利用一个专门用于分析字节码的工具:javap
常量池中的17 种数据类型的结构总表
四、访问标志
在常量池结束之后,紧接着的 2 个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息。包括:这个Class 是类还是接口、是否定义为 public 类型、是否定义为 abstract 类型、如果是类的话,是否被声明为 final、等等。
access_flags 中一共有 16 个标志位可以使用,当前只定义了 9 个。
示例的 TestClass 文件是一个普通的 Java 类文件,不是接口、枚举、注解或者模块、被public 关键字修饰但没有被声明为 final 和 abstract。
ACC_PUBLIC 和 ACC_SUPER 标志为真,因此它的 access_flags 的值应为:0x0001 | 0x0020 = 0x0021
五、类索引、父类索引与接口索引集合
Class文件由这三项来确定类的继承关系
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
Java 语言不允许多继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 之外,所有的 Java 类的父类索引都不为0 - 接口索引集合用来描述这个类实现了哪些接口
从图中可以看出,类索引、父类索引、接口索引分别为0x0008,0x0002,0x0000,即类索引为8,父类索引为2,接口索引集合大小为0。再看常量池的内容正好对上了。
对于接口索引集合,入口的第一项 u2 类型的数据为 接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器为0,后面接口的索引表不再占有任何字节。
六、字段表集合
字段表用于描述接口或者类中声明的变量
字段修饰符 放在 access_flags 中,是一个u2 的数据类型,其中可以设置标志位,可以设置的字段访问标志如下图:
跟着 access_flags 的是两项索引值:name_index 和 descriptor_index ,都是对常量池的引用,分别代表着字段的简单名称和以及字段和方法的描述符。
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值
对于 TestClass 文件
如下图第一个 u2 类型的数据为容量计数器 fields_count,值为 0x0001,表示这个类只有一个字段表数据
如下图
紧跟着容量计数器的是 access_flag 标志,值为 0x0002,代表 private 修饰符的 ACC_PRIVATE 标志位为真,其他修饰符为假
代表字段名称的 name_index 的值为0x000B,即 11,从常量池中可以得出第 11 项常量是一个 CONSTANT_Utf8_info 类型的字符串,值为 m,
代表字段描述符的 descroptor_name 的值为 0x000C ,即12,从常量池中得出第 12 个常量是 I ,根据描述符表,可以得到是基本类型 int
根据这些信息,我们可以推断出原代码定义的字段为 “private int m”!!!
七、方法表集合
Class 文件存储格式中对方法的描述和对字段的描述采用了几乎完全一致的方式,方法表的字段如同字段表一样。
八、属性表集合
Class文件、字段表、方法表 都可以携带自己的属性表集合
详细内容看周志明《深入理解 Java 虚拟机》
参考链接:
- 《深入理解 Java 虚拟机》
- https://louluan.blog.csdn.net/article/details/39960815
- https://blog.csdn.net/qq_36714200/article/details/108908510