类文件结构
概述
我们所编写的每一行代码,要在机器上运行,最终都需要编译成二进制的机器码 CPU 才能识别。但是由于虚拟机的存在,屏蔽了操作系统与 CPU 指令集的差异性,类似于 Java 这种建立在虚拟机之上的编程语言通常会编译成一种中间格式的文件来存储,比如我们今天要聊的字节码(ByteCode
)文件。
什么是字节码指令?
Java
虚拟机的指令由一个字节长度的、代表着某种特定操作含义的操作码(opcode
)以及跟随其后的零至移个代表此操作所需参数的操作数(operand
)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
比如:操作码(操作数)
解读方式
如何解读供虚拟机解释执行的二进制字节码?
- 一个一个二进制的看。用的是
Notepad++
,需要安装一个HEX一Editor
插件,或者使用Binary Viewer
- 使用
javap
指令:jdk
自带的反解析工具,终端输入以下指令
javap -v xxx.class 写入文件 javap -v xxx.class >xxx.txt
- 使用
IDEA
插件:jclasslib
或jclasslib bytecode viewer
客户端工具。(可视化更好)
文件结构
Class
文件是一组以 8 位字节
为基础单位的二进制流,各个数据严格按照顺序紧凑的排列在 Class 文件中,中间无任何分隔符,这使得整个 Class 文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用 8 位字节以上空间的数据项时,会按照高位在前的方式分割成若干个 8 位字节进行存储。
Java
虚拟机规范规定 Class
文件格式采用一种类似与 C 语言结构体的伪结构体来存储数据,这种伪结构体中只有两种数据类型:无符号数
和表
。
- 无符号数属于基本的数据类型,以
u1、u2、u4、u8
来分别代表1 个字节、2 个字节、4 个字节和 8个字节
的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8
编码结构构成的字符串值。 - 表是由多个
无符号数
或者其他表
作为数据项构成的复合数据类型
,所有表都习惯性地以「_info
」结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件就是一张表,它由下表中所示的数据项构成。
魔数
Magic Number (魔数)
:Class
文件的标志- 每个
Class
文件开头的4个字节的无符号整数称为魔数(Magic Number
)它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class
文件。即:魔数是Class
文件的标识符。 - 魔数值固定为
0xCAFEBABE
。不会改变。 - 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:
Error: A JNI error has occurred, please check your installation and try again Exception in thread “main” java.lang.ClassFormatError: Incompatible magic value 1885430635 in classfile StringTest
版本号
- 紧接着魔数的4个字节存储的是Class文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号
minor_version
,而第7个和第8个字节就是编译的主版本号major_version
。 - 它们共同构成了
class
文件的格式版本号。譬如某个Class
文件的主版本号为M
,副版本号为m
,那么这个Class
文件的格式版本号就确定为M.m
常量池
主版本号之后是常量池入口,常量池可以理解为 Class
文件之中的资源仓库,它是 Class
文件结构中与其他项目关联最多的数据类型,也是占用 Class
文件空间最大的数据项目之一,同是它还是 Class
文件中第一个出现的表类型数据项目。
因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型的数据来表示常量池的容量constant_pool_count
,和计算机科学中计数的方法不一样,这个容量是从 1 开始而不是从 0 开始计数。之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达不引用任何一个常量池项目的含义,这种情况可以把索引值置为 0 来表示。
Class
文件结构中只有常量池的容量计数是从 1 开始的,其它集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数都是从 0 开始。
常量池中主要存放两大类常量:字面量和符号引用。
- 字面量比较接近
Java
语言层面的常量概念,如字符串、声明为final
的常量值等。 - 符号引用属于编译原理方面的概念,包括了以下三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量类型和结构:
描述符:
访问标志
紧接着常量池之后的两个字节代表访问标志(access_flag
),这个标志用于识别一些类或者接口层次的访问信息,包括这个 Class
是类还是接口;是否定义为 public
类型;是否定义为 abstract
类型;如果是类的话,是否被申明为 final
等。具体的标志位以及标志的含义见下表:
类索引、父类索引与接口索引集合
类索引(this_class
)和父类索引(super_class
)都是一个 u2 类型的数据,而接口索引集合(interfaces
)是一组 u2
类型的数据集合,Class
文件中由这三项数据来确定这个类的继承关系。
- 类索引用于确定这个类的全限定名
- 父类索引用于确定这个类的父类的全限定名
- 接口索引集合用于描述这个类实现了哪些接口
字段表
fields_count
(字段计数器)
fields_count
的值表示当前class
文件fields
表的成员个数。使用两个字节来表示。fields
表中每个成员都是一个field_info
结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。
-
字段表集合(
field_info
)用于描述接口或者类中声明的变量。字段(field
)包括类变量和实例变量,但不包括方法内部声明的局部变量。字段表的结构:
解释:类型 名称 含义 数量 u2 access_flags 访问标志 1 u2 name_index 字段名索引 1 u2 descriptor_index 描述符索引 1 u2 attrubutes_count 属性计数器 1 attribute_info attributes 属性集合 attributes_count
方法表集合
methods_count
(方法计数器)
methods_count
的值表示当前class
文件methods
表的成员个数。使用两个字节来表示。
methods
表中每个成员都是一个method_info
结构。
methods:
指向常量池索引集合,它完整描述了每个方法的签名。
在字节码文件中,每一个
method_info
项都对应着一个类或者接口中的方法信息。比如方法的访问修饰符(public
,private
或protected
),方法的近回值类型
以及方法的参数信息
等。如果这个方法不是抽象的或者不是native
的,那么字节码中会体现出来。一方面,methods
表只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。另一方面,methods
表有可能会出现由编译器自动添加的方法,最典型的便是编译器产生的方法信息(比如:类初始化方法clinit
和实例初始化init
)。
属性表集合
attributes_count
(属性计数器)
attributes_countclass
的值表示当前文件属性表的成员个数。属性表中每一项都是一个attribute_info
结构
attribute_info
属性表
在
Class
文件、字段表、方法表中都可以携带自己的属性表(attribute_info
)集合,用于描述某些场景专有的信息。属性表集合不像 Class文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机在运行时会略掉它不认识的属性。
参考博客:
- https://juejin.cn/post/6913784171391352839
- https://thinkwon.blog.csdn.net/article/details/103835168
Javap指令解析
https://juejin.cn/post/6913784500040400904#heading-5