【JVM】Class文件结构简介

184 阅读18分钟

字节码是什么

源代码经过Java前端编译器编译之后便会生成一个Class文件,Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。它的内容是JVM的指令,而不像C、C++经由编译器直接生成机器码。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种微结构中只有两种数据类型:无符号数

无符号数

无符号数是基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UT℉-8编码构成字符串值。

表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

字节码文件结构

字节码可以用如下数据项构成,共有16小项。下表中,不以u开头的类型描述的是同意类型但数量不确定的连续数据集合,需要和其他字段的数量配合解析。

ClassFile {
// 类型             名称
    u4             magic;                                       // 魔数
    u2             minor_version;                               // Class文件次版本
    u2             major_version;                               // Class文件主版本
    u2             constant_pool_count;                         // 常量池数量
    cp_info        constant_pool[constant_pool_count-1];        // 常量池表
    u2             access_flags;                                // 访问标志
    u2             this_class;                                  // 类索引
    u2             super_class;                                 // 父类索引
    u2             interfaces_count;                            // 接口数量
    u2             interfaces[interfaces_count];                // 接口表
    u2             fields_count;                                // 字段数量
    field_info     fields[fields_count];                        // 字段表
    u2             methods_count;                               // 方法表数量
    method_info    methods[methods_count];                      // 方法表
    u2             attributes_count;                            // 属性表数量
    attribute_info attributes[attributes_count];                // 属性表
}

将上表总结成图如下所示,图中将集合项与集合项配合查看的数据项标了相同的颜色

image-20230205123110951

字节码查看工具

二进制工具

  • 推荐大家使用NotePad--以二进制文件打开的方式查看,查看效果如下图所示

image-20230205223552759

人性化工具

  • javap指令或者在idea中菜单栏view->Show Bytecode查看

    • javap命令常用参数-verbose:展示字节码详细信息

image-20230205224202522

  • IDEA jclasslib插件

jclasslib插件展示的字节码信息通过文件进行了分类,并且可以跳转到引用的常量池。如果对某个指令不熟悉,还可以点击跳转到JVM文档

image-20230205223820334

  • Jclasslib ByteCode Viewer

查看效果与IDEA jclasslib插件相同

image-20230205223635621

字节码结构详解

下面字节码结构我们拿一个简单的java代码反编译举例

public class Child extends Parent implements Serializable,Cloneable {
    private int age;
    private final String name="小张";
    public int getAge() {
        return age;
    }
}

魔数

首先每个Class文件的头4个字节称为魔数(Magic Number) ,它的值固定为0xCAFEBABE。魔数的作用是确定该文件是否是Class文件。其他文件存储标准中也采用了相似的设计,如gif和jpeg。字节码魔数如下图所示

image-20230205231707382

版本号

其次版本号是由minor_versionmajor_version共同构成,如果某个Class文件的主版本号为M,服版本号为m,那么这个Class文件的版本呢号为M.m

主版本号和编译器版本号对应关系如下,Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1

不同版本的]ava编译器编译的c1ass文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。(向下兼容)

Java SEReleasedMajorSupported majors
1.0.2May-19964545
1.1Feb-19974545
1.2Dec-19984645 .. 46
1.3May-20004745 .. 47
1.4Feb-20024845 .. 48
5Sep-20044945 .. 49
6Dec-20065045 .. 50
7Jul-20115145 .. 51
8Mar-20145245 .. 52
9Sep-20175345 .. 53
10Mar-20185445 .. 54
11Sep-20185545 .. 55
12Mar-20195645 .. 56
13Sep-20195745 .. 57
14Mar-20205845 .. 58
15Sep-20205945 .. 59
16Mar-20216045 .. 60
17Sep-20216145 .. 61
18Mar-20226245 .. 62
19Sep-20226345 .. 63

例如下图中minor_version的值为00 00major_version的值为00 34,16进制的34转换为10进制的值为52,通过主版本号得知编译这个Class文件的Java版本号为Java8。

image-20230205232023212

常量池

紧接着主版本号的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它也是字节码文件中被关联最多的数据类型,也是直接码文件中占用文件空间最大的部分,随着Java虚拟机的不断发展,常量池的容也日渐丰富。可以说,常量池是整个Class文件的基石。

常量池包括常量池数量常量池表构成。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的无符号数,代表常量池容量计数值(constant_pool_count)。

常量池数量与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的。

字符串常量池整体结构整体结构如下图所示

image-20230205231347127

目前Java虚拟机定义了14中类型的常量,这些常量名都是以CONSTANT开头,_分隔,info结尾,每种类型常量池如图所示

image-20230205120014384

常量池中主要存放的内容

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

字面量主要包括:

  • 文本字符串
  • 声明为final的常量值

符号引用主要包括

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
全限定名

全限定名为包名+类名,仅仅是把包名的"."替换成"/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。具体如下图所示

image-20230205234007740

简单名称

简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的getAge()方法和age字段的简单名称分别是add和num。

image-20230205233855732

描述符

描述符的作用是用来描述它段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:

标志符含义
B基本数据类型byte
c基本数据类型char
D基本数据类型double
F基本数据类型float
I基本数据类型int
j基本数据类型long
s基本数据类型short
z基本数据类型boolean
v代表void类型
L对象类型,比如: Ljava/lang/object;
[数组类型,代表一维数组。比如:double[][][] is [[[D

虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引引用,并译到具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联

  • 符号引用: 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用: 直接用可以是直接指问目标的指针、相对扁移量或是一个指间接定位到目标的句柄。直接引用是与虚以机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定己经存在于内存之中了。

访问标识(access_flag、访问标志、访问标记)

在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。各种访问标记如下所示:

标志名称标志值含义
ACC_PUBLIC0x0001标志为public类型
ACC_FINAL0x0010标志被声明为final,只有类可以设置
ACC_SUPER0x0020标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法)
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC0x1000标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应)
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUM0x4000标志这是一个枚举

需要补充说明的是

  • 类的访问权限通常为ACC开头的常量。
  • 字节码中的访问标识是表格中访问标识的加和。
  • 带有ACC_INTERFACE标志的class文件表示的是接口而不是类,反之则表示的是类而不是接口。

    • 如果一个class文件被设置了ACC_INTERFACE标志,那么同时也得设置ACC_ABSTRACT标志。同时它不能再设置ACC_FINAL、ACC_SUPER或ACC_ENUM标志.
    • 如果没有设置ACC_INTERFACE标志,那么这个Class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT。这类互斥的标志除外。这两个标志不得同时设置。
  • AcC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对门ava虚拟机指令集的编译器都应当设置这个标志。对于]ava SE8及后续版本来说,无论class文件中这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志。

    • ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的ACC_SUPER标志在由JDK1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么Oracle的Java虚拟机实现会将其忽略。
  • ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
  • 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
  • ACC_ENUM标志表明该类或其父类为枚举类型。

Child是一个普通的Java类,不是接口、枚举或者注解,被public关键字修饰,但没有被声明为final和abstract,并且它使用了JDK1.2之后的编译器进行编译,因此access_flags为0x0021

image-20230205234915457

类索引、父类索引、接口索引集合

在访问标记后,会指定该类的类别、父类类别以及实现的接口,类索引(this class)和父类索引(super class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的)java类都有类,因此除了java.lang.object外,所有Java类的父类索引都不为0。类索引、父类索引、接口索引结构见下图

image-20230206001640498

这三项数据来确定这个类的继承关系。

  • 类索引用于确定这个类的全限定名

    • 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
    • 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
this_class(类索引)

类索引是2字节无符号整数,指向常量池的索引。它提供了类的全限定名,如java.lang.Thread。this_class的值必须是对常量池表中某项的一个有效索引值。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个c1ass文件所定义的类或接口。

super_class(父类索引)

父类索引是一个2字节无符号整数,指向常量池的索引。它提供了当前类的父类的全限定名。如果我们没有继承任何类,其默认继承的是java/lang/Object类。同时,由于Java不支持多继承,所以其父类只有一个。superclass指向的父类不能是final。

interfaces
  • 指向常量池索引集合,它提供了一个符号引用到所有己实现的接口
  • 由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(当然这里就必须是接口,而不是类)。
interfaces_count(接口计数器)

interfaces_count项的值表示当前类或接口的直接超接口数量。

interfaces

interfaces[]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfaces_count。每个成员interfaces[i]必须为CONSTANT_Class_info结构。在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。

字段表

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

  • 用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等

注意事项:

  • 字段表集合中不会列出从父类或者实现的接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
  • 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
字段表结构

字段表作为一个表,同样有他自己的结构,它主要包括字段数量字段表集合两大组成部分

image-20230206003256997

字段数量

fields_count的值表示当前class文件fields表的成员个数。使用两个字节来表示。

字段表(fields[])

fields表中的每个成员都必须是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完整描述。 一个字段的信息包括如下这些信息。这些信息中,各个修饰符都是布尔值,要么有,要么没有。

  • 作用域(public、private、protected修饰符)
  • 是实例变量还是类变量(static修饰符)
  • 可变性(final)
  • 并发可见性(volati1e修饰符,是否强制从主内存读写)
  • 可否序列化(transient修饰符)
  • 字段数据类型(基本数据类型、对象、数组)
  • 字段名称
字段访问标志(access_flags)

字段的access_flags与类中的access_flags非常相似,都是一个u2的数据类型

标志名称标志值含义
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLATILE0x0040字段是否为volatile
ACC_TRANSTENT0x0080字段是否为transient
ACC_SYNCHETIC0x1000字段是否为由编译器自动产生
ACC_ENUM0x4000字段是否为enum
字段描述符标识字符(descriptor_index)

字段描述符是用来描述字段的数据类型,字段描述符含义参考下面表

标志符含义
B基本数据类型byte
C基本数据类型char
D基本数据类型double
F基本数据类型float
I基本数据类型int
J基本数据类型long
S基本数据类型short
Z基本数据类型boolean
V代表void类型,用于方法的描述符
L对象类型,比如:Ljava/lang/Object;
[数组类型,代表一维数组。比如:double[][][] is [[[D

上述代码例子中字段表中name的信息如下

image-20230206004805342

查看name的信息显示了name的名称,字段描述符引用了常量池中的Ljava/lang/String

image-20230206004820020

方法表

方法表是字节码对Class文件中方法的描述,它在字节码文件中的存储格式与字段的描述几乎采用了完全一致的方式,具体结构如下图

image-20230206004419584

方法描述符

方法中的描述符与字段的描述符不同,方法的描述符主要是用来描述方法的参数列表(包括数量、类型以及顺序)和返回值。具体描述符含义参考字段描述符含义表。

其他部分与字段表几乎相同,这里就不在过多赘述。例子中使用jclasslib插件查看方法表中信息如下图,方法表中有两个方法,其中<init>方法是由编译器生成的

image-20230206004909795

我们打开getAge方法表,显示了该方法的名字,方法描述符显示这是一个无传参,返回值为int类型的方法

image-20230206004600418

属性表

属性表(attribute info)在Class文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与clss文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,《Java虚拟机规范(第2版)》中预定义了9项虚拟机实现应当能识别的属性,而在《Java虚拟机规范(Java SE7)》版中,预定义属性已经增加到21项。

属性结构如下图所示,其中属性信息字段根据属性的不同,结构的长度也不相同

image-20230206010157644

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情