前言

计算机只能识别01组成的二进制码,在没有跨平台语言的时期,把开发程序编译成二进制是唯一选择。Java从设计之初就是为了跨平台而生,之所以可以跨平台,是因为Java虚拟机提供了跨平台实现,屏蔽了因硬件、操作系统等带来的差异。随着大量建立在虚拟机之上的语言诞生,把程序编译成二进制机器码已不再是唯一选择,从本地机器码转变为字节码,是存储格式发展的一小步,却是Java语言发展的一大步。
image.png
众所周知,Java虚拟机所能运行的是.class字节码文件,Java源文件通过Java编译器编译后存储为字节码格式的文件,Java虚拟机规范中定义了字节码文件的格式、构成等,那么Class文件的内部结构到底是什么样子的?

  1. public class Hello {
  2. public static void main(String[] args) {
  3. System.out.println("Hello World");
  4. }
  5. }

很多人学习Java是从这里开始,那就从这里从新一探字节码的究竟。

文件格式

在Java虚拟机规范中明确定义了Class文件的格式、以及存储结构等信息,任何一个Class文件都对应着唯一的接口或者类信息,反过来就不成立,因为类或者接口是可以动态生成并加载的。
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在文件中,文件中没有任何分割符,整个Class文件中都是运行时的必要数据,没有多余空隙,遇到占用超过8个字节以上的数据项时,按照高位在前的方式分割成若干个8字节进行存储。
Java虚拟机规范官方文档定义,Class文件中采用类似于C语言结构体的伪结构存储数据,只有两种类型,一类是表,一类是无符号数。

  • 无符号数:属于基本数据类型,以u1、u2、u4、u8来表示1、2、4、8个字节
  • 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表以_info结尾,用于描述层次关系,整个Class文件也可以看作一张顺序表。

Java虚拟机规范定义的Class文件结构如下所示:

  1. ClassFile {
  2. u4 magic;
  3. u2 minor_version;
  4. u2 major_version;
  5. u2 constant_pool_count;
  6. cp_info constant_pool[constant_pool_count-1];
  7. u2 access_flags;
  8. u2 this_class;
  9. u2 super_class;
  10. u2 interfaces_count;
  11. u2 interfaces[interfaces_count];
  12. u2 fields_count;
  13. field_info fields[fields_count];
  14. u2 methods_count;
  15. method_info methods[methods_count];
  16. u2 attributes_count;
  17. attribute_info attributes[attributes_count];
  18. }

说明:_info结尾的表示表,表其后的 [ ] 内的内容表示表预定义的长度

魔数

每个Class文件的前4个字节被称为魔数(Magic Number),它的唯一作用是用来确定这个文件是否一个可以被虚拟机认可的Class文件,由于文件名后缀的可替代性,因此使用魔数来作为文件格式的认证标志。类似的GIF或者JPEG等文件也在头部定义了魔数,而Java的魔数值也是富有浪漫气息的,被称为0xCAFEBABE(咖啡宝贝)。

版本号

魔数之后是Class文件的版本号,第5、6字节是minor_version(次版本号),第7、8字节是major_version(主版本号)。Java版本号从45开始,JDK1.1之后每个大版本发布主版本号加一,JDK1.0~JDK1.1使用了45.0~45.3。高版本的JDK可以向下兼容,但是不能向上执行Class文件,Java虚拟机规范规定,在Class文件校验阶段,明确要求即使这个文件可以被解析,虚拟机必须拒绝超过其版本号的Class文件。JDK 8所对应的主版本号为52。
关于魔数和版本号查看下面一张图片示例:
image.png

常量池

常量池排布在魔数和版本号之后,是Class文件中第一个表类型数据项,可以看作Class文件的仓库,是Class文件中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项之一。字符串字面量在编译阶段通常都会被装载到常量池(这里的常量池所指的是Class文件常量池,这也是空间占用较大的原因之一),而在类加载后才会进入运行时常量池。由于常量池数据量不固定,因此在常量池表之前通过u2类型的constant_pool_count来表示常量池的计数容量,计数容量的值从1开始。

Java代码编译过程中,不会在Class字节码文件中保存方法、字段的布局信息,而是在虚拟机加载的时候进行动态连接,字段以及符号引用等也是这时从常量池中被翻译到具体的内存中。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。

  • 字面量指Java中的文本字符串,被声明为final的常量值等;
  • 符号引用属于编译方面的概念,主要包含以下几类常量
    • 被模块导出或者开放的包(package)
    • 类和接口的全限定名称(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符
    • 方法疾病和方法类型(Method Handle、Method Type、Invoke Dynamic)
    • 动态调用和动态常量(Dynamic Computed Call Site、Dynamic Computed Constant)

常量池中的每一项常量都是一个表,这些表结构都具有共同的特点,表结构第一位是u1类型的标志位(tag),表示当前常量属于哪种类型。查看Java虚拟机规范8,各种类型所对应的标志位如下:
image.png
除了第一位的标志位以外,还包含常量池索引name_index,指向常量池中的一个CONSTANT_Utf8_info类型的常量。而CONSTANT_Utf8_info常量由如下C结构体组成,查看CONSTANT_Utf8_info结构体的具体内容:

  1. CONSTANT_Utf8_info {
  2. u1 tag;
  3. u2 length;
  4. u1 bytes[length];
  5. }
  • u1 表示一个无符号字节,u2表示2个无符号字节,以此类推。
  • tag 1个字节的标志位,value(1),表示这是一个CONSTANT_Utf8_info结构的常量。
  • length 表示存储字符串内容的长度,有多少个字节,u2表示2个字节,因此字符串所能表示的最大长度为2^16-1=65535,Java中所有方法、变量名超过了这个长度就无法编译。
  • bytes 表示存储字符串内容的字节数组,可能包含很多个字节。

以下来查看以下最常用的String类型在Class文件中的格式,其所对应的结构是CONSTANT_String_info:

  1. CONSTANT_String_info {
  2. u1 tag;
  3. u2 string_index;
  4. }
  • tag 一个字节标志位,对应上述表中CONSTANT_String的值为8,表示CONSTANT_String_info结构的常量。
  • string_index必须是constant_pool表中包含的有效索引,索引下entry必须为CONSTANT_Utf8_info结构体。

String类型的常量在常量池的布局如下示意图所示:
image.png
关于其他类型的结构及布局可以参考Java虚拟机规范,第4章节The Class File Format。

访问标志

常量池之后的2个字节access_flags代表访问标志,这个标志用于识别类或者接口的层次访问信息,包括Class所指的是类还是接口,是否为public类型,是否为abstract类型,如果是类是否声明为final等场景。具体标志位信息如下

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否是public类型
ACC_FINAL 0x0010 是否被声明final,仅类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令新语义,JDK1.0.2之后都为真
ACC_INTERFACE 0x0200 标识接口
ACC_ABSTRACT 0x0400 标识abstract类型,接口、抽象类为真,其他为假。
ACC_SYNTHETIC 0x1000 标识类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识注解
ACC_ENUM 0x4000 表示枚举
ACC_MODULE 0x8000 标识模块

说明:access_flags共16个标志位可用,目前用到了9个,未用到的标志位一律为0。

索引

索引这里所列举的是指类索引(this_class)、父类索引(super_class)、以及接口索引(interfaces),按照顺序排列在访问标志之后,其中类索引和父类索引都是u2类型的数据,接口索引是一组u2类型的数据集合。

类索引用于确定类的全限定名称,父类索引用于确定父类的全限定名称,由于Java不支持多继承,支持多实现的特性,因此父类索引是一个u2类型数据,而接口是一组u2类型数据集合。除了java.lang.Object类外,类都默认继承了Object类,因此所有Java类的父类索引不能为0。

类索引和父类索引都指向了一个类型为CONSTANTClass_info的类描述符常量,通过CONSTANT_Class_info类型的常量索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名称字符串。
![录屏
选择区域20220421002533.gif](https://cdn.nlark.com/yuque/0/2022/gif/2456868/1650471973873-43b9435b-107e-48b2-a035-76d2ed5a8482.gif#clientId=uefdb396c-6996-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=uf49c5b5c&margin=%5Bobject%20Object%5D&name=%E5%BD%95%E5%B1%8F%E9%80%89%E6%8B%A9%E5%8C%BA%E5%9F%9F_20220421002533.gif&originHeight=664&originWidth=1810&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1885273&status=done&style=none&taskId=uce7d2ccc-74be-4952-a9ab-31cacc1802d&title=)

  • 常量池中索引[05]this_class:指向索引[27] com/starsray/skywalking/HelloWorld
  • 常量池中索引[06]super_class:指向索引[28] java/lang/Object

对于接口索引集合,包含u2类型的接口索引计数interfaces_count,表示接口索引列表容量,如果没有实现任何接口,该记录为0,索引表将不占用任何字节。

字段表

字段表(field_info)用来描述接口或者类中声明的变量。字段表中变量包含类(static)变量和成员变量,不包含方法中出现的局部变量。字段表中存在的变量名可以使用各种修饰符,如访问控制修饰符(private、protected、public)、可变性(final)、并发可见性(volatile)、序列化(transient)等,这些修饰符的值都是布尔类型,要么有要么没有,使用标志位来表示。
字段除了包含修饰符以外,其余如字段名、数据类型都是无法固定的,只能通过引用常量池中的常量来描述。如下展示了字段表的最终格式:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

字段表的access_flags与类的access_flags类似,都是u2数据类型表示,其所包含的表示形式如下

标志名称 含义
ACC_PUBLIC 0x0001 是否public
ACC_PRIVATE 0x0002 是否private
ACC_PROTECTED 0x0004 是否protected
ACC_STATIC 0x0008 是否static
ACC_FINAL 0x0010 是否final
ACC_VOLATILE 0x0040 是否volatile
ACC_TRANSIENT 0x0080 是否transient
ACC_SYNTHETIC 0x1000 是否由编译器自动产生
ACC_ENUM 0x4000 是否enum

name_index和descriptor_index都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。

说明:描述符:描述字段的数据类型、方法的参数列表(包含数量、类型、顺序、返回值),八大基本数据类型和void以一个大写字符来表示,对象类型使用L加对象全限定名称来表示。如:

  • B:基本类型byte
  • C:基本类型char
  • D:基本类型double
  • F:基本类型float
  • I:基本类型int
  • J:基本类型long
  • S:基本类型short
  • Z:基本类型boolean
  • V:特殊类型void
  • L:对象类型,如:Ljava/lang/Object

方法表

Class文件格式中,对方法描述与字段描述几乎采用了完全一致的形式。依次包含访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)等几项。与字段表区别在于一些访问标志及修饰符的限制。例如:volatile和transient不能修饰方法。

属性表

属性表(attribute_info),Class文件、字段表、方法表都可以携带自己的属性表集合,用来描述某些特有信息。属性表与Class文件中其他数据项目严格要求顺序有所不同,其限制相对宽松。Java虚拟机规范定义属性表的定义遵循以下格式:

  1. attribute_info {
  2. u2 attribute_name_index;
  3. u4 attribute_length;
  4. u1 info[attribute_length];
  5. }

对于每一个属性,它的名称都与常量池中一个CONSTANT_Utf8_info类型的数据相对应,而属性值的结构可以自定义实现,通过u4长度属性说明其占位长度。
关于属性表中所定义的内容如下所示:图摘自深入Java虚拟机第三版
AF0D9443ADAB49CBFB03EB8F87C950F1.jpg
C62042A959E9CC81307FFD79E5D5BA6C.jpg
其中列出了众多属性定义,以LocalVariableTable属性为例,自定义结构如下表所示:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

关于前几个名称就不在解释,local_variable_info表定义结构如下所示:

类型 名称 数量
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

start_pc和length分别代表了局部变量表声明周期开始的字节码偏移量及其作用覆盖长度,二者结合起来就是局部变量表在字节码中的作用域范围。
name_index和
descriptor_index指向常量池中CONSTANT_Utf8_info类型的常量的索引,分别表示局部变量表的名称以及局部变量的描述符。
index表示局部变量在栈帧的局部变量表中变量槽的位置。
更多关于属性表的详细信息参考Java虚拟机规范中第4.7 Attributes部分。

参考文档: