本篇博客主要学习了JVM生成的Class文件结构。
字节码概要
Java设计之初有个非常著名的口号“一次编写,到处运行”,虽然如今有很多新语言都实现了这个特性(如Python,Go等),但在那个年代这个口号无疑带来了巨大的意义。那么是什么让Java实现了跨平台的能力呢?答案就在字节码。通过各种不同平台的Java虚拟机,编译得到的字节码文件能够处处运行。
Jvm编译Java代码时得到的字节码文件是以Class为单位的。Class文件是一组以8个字节为基础单位的二进制流,各个数据项严格按照顺序紧凑排列在一起。对于8个字节以上空间的数据项,Class会拆成若干个8个字节并按照高位在前的顺序排列,并且Class文件的字节序是大端序。Class文件使用两种伪结构来存储数据:
- 无符号数:如u1、u2、u4、u8分别代表1、2、4、8个字节,可用来描述数字、引用、数量值或utf-8编码构成的字符串值。
- 表:由多个无符号数或者其他表作为数据项构成的符合数据类型。为了方便辨认,表的后缀都是“_info”结尾。
Class文件的结构如下图所示:
这里简单介绍下各个数据项的含义:
- magic:值是固定的,0xCAFEBABE(咖啡宝贝)
- minor_version:次版本号
- major_version:主版本号
- constant_pool_count:该值-1为常量池数量
- constant_pool:常量池
- access_flags:掩码标识,表明访问权限和该类/接口的属性。
- this_class、super_class、interfaces_count和interfaces:分别表示类索引、父类索引和接口索引集合,并通过这三者确定当前Class的继承关系。
- fields_count和fields:表明了声明的变量,包括了类变量和实例变量。
- methods_count和methods:表明了声明的方法。
- attributes_count和attributes:属性表集合。
简单的代码示例
下面将使用一个简单的类来说明Java类文件机制。
1 | package twd; |
对该java类进行编译,得到对应的class文件,其16进制显示如下:
详细说明
下面将结合示例代码和对应class文件进行各字段的详细说明。
magic
魔法值,长度为4字节,固定为cafe babe。
次版本号和主版本号
次版本号和主版本号的长度均为2字节,次版本号再Jdk1.2到JDK12之前均未被使用(均为0),主版本号则指明了相应的JDK大版本号。两者在该示例中的值分别为0x0000和0x0034(十进制为52,对应JDK8)。
常量池数量和常量池
紧接着的2字节为(常量池数量-1),该示例中常量池数量为0x0021-1,即有32项索引。注意,在设计之初就把索引0空出来是有特殊考虑的,如果后面某些指向常量池的索引值的数据在特定情况下
需要表达“不引用任何一个常量池项目”的含义, 可以把索引值设置为0来表示。
常量池中的常量可分为两大类型:
- 字面量:如文本字符串、声明为final的值等。
- 符号引用:属于编译原理方面的概念,又可继续往下细分
- 被模块导出或开放的包:package
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
当虚拟机进行类加载时,会从Class常量池获得对应的符号引用,并在类创建时或运行时解析、翻译到具体的内存地址。
常量池数量后为具体的常量。常量的格式由两部分组成:
- u1类型的标志位:代表当前常量属于哪种常量类型
- 特定的常量表结构:特定于不同常量类型的表结构
最初常量表中共有11种结构各不相同的表结构数据,Jdk8中共有14中常量池类型(如图3.1所示)。截止Jdk13,已经有了17种表结构。接下来仅介绍在示例代码中出现的常量表结构,如果想更详细地了解,可阅读Java虚拟机规范。
第一个常量表结构的标志为0x0a,即为CONSTANT_Methodref_info,其格式为:
1 | CONSTANT_Methodref_info { |
示例中对应的class_index为0x0006(6),name_and_type_index为0x0013(19)。
第二个常量表结构的标志为0x09,即为CONSTANT_Fieldref,其格式为:
1 | CONSTANT_Fieldref_info { |
示例中对应的class_index为0x0014(20),name_and_type_index为0x0015(21)。
第三个常量表结构的标志为0x08,即为CONSTANT_String_info,其格式为:
1 | CONSTANT_String_info { |
示例中对应的string_index为0x0016(22)。
后续的常量池不一一介绍,先直接通过javap输出对应的常量池信息。
再介绍下之前没出现的几个常量表结构。其中第五个常量表结构为CONSTANT_Class_info。其格式为:
1 | CONSTANT_Class_info { |
可以看到,第五个常量的name_index为25,指向twd.Test这个类名。
第七个常量表类型为CONSTANT_Utf8_info,其格式为:
1 | CONSTANT_Utf8_info { |
再来看下第19个常量表类型,是CONSTANT_NameAndType_info,其格式为:
1 | CONSTANT_NameAndType_info { |
该常量指的是类的构造函数<init>,()V表示无参且无返回值。
访问标志
接下来是2字节的访问标识,示例中值为0x0021。根据下面的访问标志定义可示例Class的标识为ACC_PUBLIC和ACC_SUPER。
索引
接下来是各种索引:类索引(this_class)、父类索引(super_class)和接口集。
类索引
其值为0x0005,指向twd.Test这个CONSTANT_Class_info。
父类索引
其值为0x0006,表示父类为java.lang.Object。
接口集
在这里接口数量为0x0000,所以无实现接口。
字段表
示例中字段表数量为0x0001,后续使用field_info表示每个字段。field_info的格式为:
1 | field_info { |
这里对应的值为:
1 | access_flags:0x0002,即ACC_PRIVATE |
显然该字段对应的是代码中的private int a
。
方法表
示例中方法表数量为0x0002,表示有两个方法。每个方法的格式与field_info一样。
第一个方法对应的值为:
1 | access_flags:0x0001,即ACC_PUBLIC |
对于属性,其对应的格式为:
1 | attribute_info { |
该属性的attribute_name_index为0x000b,即常量池中第11个常量Code,其长度为0x0000002f,对应的内容如下:
属性表
属性表用来描述某些专有信息,可出现在类文件、字段表和方法表中。用户可自定义属性格式,虚拟机会在运行时忽略它不认识的属性。在Jdk8中,预定义了23个属性:
比较常见的属性有Code(附在方法表中,主要表示方法执行代码的字节码形式)、Exceptions等,想要详细了解的可参考《深入理解Java虚拟机》的6.3.7。
参考资料
- 深入理解Java虚拟机(第三版)
- Java虚拟机规范