# 类文件结构

我们都知道Java虚拟机执行的是class文件中的字节码(Byte Code),但为什么要存在这样一个中间语言呢?不能直接把Java语言编译成本地机器码(Native Code)吗?

答案就是平台无关性,Java在诞生之初就提出要“一次编写,到处运行(Write Once,Run Anywhere)”,这句话充分表达了当时软件开发人员对跨平台开发的需求之渴望,各种不同的硬件体系结构,各种不同的操作系统,都可以运行着Java虚拟机,上面载入并执行同一种平台无关的字节码

甚至,设计者们曾考虑到虚拟机的语言无关性,让其他语言也能够运行在Java虚拟机上,他们在发布规范文档的时候,特地把规范拆分成《Java语言规范》(The Java Language Specification)和《Java虚拟机规范》(The Java Virtual Machine Specification),如今,Kotlin、Clojure、Groovy、JRuby、JPython、Scala等语言,也的确得益于这种高瞻远瞩的设计构想而诞生了

而实现语言无关性的基础,仍然是虚拟机和字节码存储格式,Java虚拟机不与任何程序语言做绑定(包括Java),它只与class文件这种特定的二进制格式所关联

image-20210816194209939

# Class类文件的结构

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储

根据《Java虚拟机规范》的规定,Class文件采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:“无符号数”和“表”

  • 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
  • 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有的表的命名都习惯性以“_info”结尾。表用于描述有层次关系的复合结构数据,整个Class文件也可以视作一张表,详见下表

Class文件格式

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会用一个前置的容量计数器加若干个连续数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合

# 魔数与Class文件的版本

  • magic
  • minor_version
  • major_version

魔数在每个Class文件的最开始4个字节,它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件,不仅仅Class文件,很多文件格式标准都有使用魔数来标识的习惯,使用魔数而非文件后缀名,主要出于安全考虑,因为文件后缀名可被随意改动,Class文件的魔数值为0xCAFEBABE(咖啡宝贝)

紧接着魔数的4个字节分别是Class文件的次版本号(Minor Version)和主版本号(Major Version),Java的版本号是从45开始的,JDK1.1之后每个JDK大版本往上加一,高版本的JDK能向下兼容之前版本的Class文件,但不兼容之后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件

# 常量池

  • constant_pool_count
  • constant_pool

紧接着主次版本号之后,就是常量池了,它通常是Class文件中占用空间最大的数据项目之一

由于常量池中常量的数量并不固定,所以需要先放置一项constant_pool_count来计数,值得注意的是,这个容量计数是从1开始而非0,也就是说constant_pool_count为0x0016,即十进制的22,那么代表常量池中有21项常量,这个设计是为了表达“不引用任何一个常量池项目”的Class文件,那么它的constant_pool_count就为0,Class文件结构中只有常量池的容量计数从1开始,对于其它集合类型,都是从0开始

常量池中主要存放了两种常量:

  • 字面量(Literal):比较接近通常说的Java语言层面的概念,比如文本字符串,final类型常量值等
  • 符号引用(Symbolic References):属于编译原理方面的概念

常量主要包含以下几类:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle,Method Type,Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site,Dynamically-Computed Constant)

在编译Java代码时,并没有和C和C++那样的连接过程,而是在虚拟机加载Class文件时候进行动态连接,会在这个阶段从常量池获得对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址中

常量池中每一项都是一个表,它们都有一个共同特点,就是表结构起始第一个都是一个u1类型的标志位,代表当前常量属于哪种类型,具体如下:

常量池中的17种数据类型的结构总表

常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 长度为length的UTF-8编码的字符串
CONSTANT_Integer_info tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Float_info tag u1 值为4
bytes u4 按照高位在前存储的float值
CONSTANT_Long_info tag u1 值为5
bytes u8 按照高位在前存储的long值
CONSTANT_Double_info tag u1 值为6
bytes u8 按照高位在前存储的double值
CONSTANT_Class_info tag u1 值为7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 值为8
index u2 指向字符串字面量的索引
CONSTANT_Fieldref_info tag u1 值为9
index u2 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
index u2 指向字段描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Interfaceref_info tag u1 值为11
index u2 指向声明方法的接口描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_NameAndType_info tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2 指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_info tag u1 值为15
reference_kind u1 值必须在1至9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对常量池的有效索引
CONSTANT_MethodType_info tag u1 值为16
descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_Dynamic_info tag u1 值为17
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_InvokeDynamic_info tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_Module_info tag u1 值为19
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名字
CONSTANT_Package_info tag u1 值为20
name_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示包名称

对Class文件的常量池信息,可以使用JDK中的javap工具配合-v选项来得到,这里不再赘述

# 访问标志

  • access_flags

在常量池之后,紧接着的2个字节就是访问标志了,它用于标识比如这个Class是类还是接口,以及它是public还是其它权限,等等

访问标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义允许在JDK1.0.2发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

access_flags占用了u2即2个字节,其中共有16个标志位可以使用,当前只定义了9个,没有使用到的标志位要求一律为零

假设一个类是public类型,且使用了JDK1.0.2之后的编译器进行编译,那么它的access_flags应为:0x0001 | 0x0020 = 0x0021

# 类索引,父类索引与接口索引集合

  • this_class
  • super_class
  • interfaces_count
  • interfaces

在访问标志之后,就是类索引,父类索引与接口索引集合

Class文件由这三项数据来确定该类型的继承关系,类索引用于确认这个类的全限定名,父类索引用于确定这个类的父类的全限定名

由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类

接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中

类索引,父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过它里边的index,可以继续找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串

# 字段表集合

  • fields_count
  • fields

字段表用于描述接口或者类中声明的变量,它包含了类级变量以及实例级变量,但不包含在方法内部声明的局部变量,包括的修饰符如下:

  • 作用域修饰符(public,private,protected)
  • 是实例变量还是类变量修饰符(static)
  • 可变性修饰符(final)
  • 并发可见性修饰符(volatile,是否强制从主内存读写)
  • 可否被序列化修饰符(transient)

以及字段数据类型和名称信息等,以上这三类信息中,修饰符可以用标志位来表示,而后两者信息都是无法固定的,只能用常量池中的常量来描述

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本代码中不存在的字段,比如在内部类中为了保持对外部类的访问性,编译器会自动添加指向外部类实例的字段

在Java语言层面,字段是无法被重载的,无论字段的数据类型,修饰符是否相同,但是在Class文件中,只要两个字段的描述符不完全相同,重名就是合法的

字段表结构

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

字段访问标志

标志名称 标志值 含义
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

描述符标识字符含义

标识字符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 特殊类型void
L 对象类型,如Ljava/lang/Object;

# 方法表集合

  • methods_count
  • methods

方法表基本上和字段表使用了完全一致的描述方式

看到这里,可能会疑惑,方法里的代码去哪里了呢?答案是在下面讲到的属性表集合的Code里,后面会细说

和字段表集合相对应,如果父类方法在子类中没有被重写(Override),那么方法表集合里就不会出现来自父类的方法信息,同样,有可能会出现编译器自动添加的方法,比如类构造器“<clinit>()”方法和实例构造器“<init>()”方法

在Java语言层面,重载方法需要不同的特征签名,包括了

  • 方法名称
  • 参数顺序
  • 参数类型

但是,在字节码层面,重载方法还额外包括了:

  • 方法返回值
  • 受查异常表

所以,如果两个方法有相同的名称和特征签名,但返回值不同,也是可以合法共存于同一个Class文件中的

方法表结构

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

方法访问标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否为public
ACC_PRIVATE 0x0002 方法是否为private
ACC_PROTECTED 0x0004 方法是否为protected
ACC_STATIC 0x0008 方法是否为static
ACC_FINAL 0x0010 方法是否为final
ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
ACC_BRIDGE 0x0040 方法是不是由编译器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为native
ACC_ABSTRACT 0x0400 方法是否为abstract
ACC_STRICT 0x0800 方法是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否由编译器自动产生

那么,方法内部的代码都存放在哪里呢?

在经过Javac编译器编译成字节码指令后,方法内部代码都被存放在了方法属性表集合中一个名为”Code“的属性里面,属性表是Class文件格式中最具拓展性的一种数据项

# 属性表集合

  • attributes_count
  • attributes

属性表集合是Class文件格式中,最具扩展性的一种数据项目

虚拟机规范预定义的属性

属性名称 使用位置 含义
Code 方法表 Java代码编译成的字节码指令
ConstantValue 字段表 由final关键字定义的常量值
Deprecated 类,方法表,字段表 被声明为deprecated的方法和字段
Exceptions 方法表 方法抛出的异常列表
EnclosingMethod 类文件 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标示这个类所在的外围方法
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
StackMapTable Code属性 JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature 类,方法表,字段表 JDK5中新增的属性,用于支持泛型情况下的方法签名。在Java语言中,任何类,接口,初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile 类文件 记录源文件名称
SourceDebugExtension 类文件 JDK5中新增的属性,用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加入的调试信息
Synthetic 类,方法表,字段表 标识方法或字段为编译器自动生成的
LocalVariableTypeTable JDK5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations 类,方法表,字段表 JDK5中新增的属性,为动态注解提供支持。该属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleAnnotations 类,方法表,字段表 JDK5中新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotations 方法表 JDK5中新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数
RuntimeInvisibleParameterAnnotations 方法表 JDK5中新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数
AnnotationsDefault 方法表 JDK5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods 类文件 JDK7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符
RuntimeVisibleTypeAnnotations 类,方法表,字段表,Code属性 JDK8中新增的属性,为实现JSR308中新增的类型注解提供的支持,用于指明哪些类注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleTypeAnnotations 类,方法表,字段表,Code属性 JDK8中新增的属性,为实现JSR308中新增的类型注解提供的支持,与RuntimeVisibleTypeAnnotations属性作用刚好相反,用于指明哪些类注解是运行时不可见的
MethodParameters 方法表 JDK8中新增的属性,用于支持(编译时加上-parameters参数)将方法名称编译进Class文件中,并可运行时获取。此前要获取方法名称(典型的如IDE的代码提示)只能通过JavaDoc中得到
Module JDK9中新增的属性,用于记录一个Module的名称以及相关信息(requires,exports,opens,uses,provides)
ModulePackages JDK9中新增的属性,用于指定一个模块的主类
NestHost JDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类
NestMembers JDK11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类

属性表结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

# Code属性

Java程序方法体里的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内

Code属性表的结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count

attribute_name_index是一项指向CONSTANT_Utf8_info类型常量的索引,常量值固定为“Code”,代表了该属性的名称

max_stack代表了操作数栈(Operand Stack)深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度

max_locals代表了局部变量表所需的存储空间,它的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存的最小单位。方法参数(包括实例方法中的隐藏参数“this”),显式异常处理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常),方法体中定义的局部变量都需要依赖局部变量表来存放,但是需要注意的是,并不是在方法中用了多少个局部变量,就把这些局部变量所占变量槽数之和作为max_locals的值,操作数栈和局部变量表字节决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费,虚拟机的做法是将局部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据同时生存的最大局部变量数量和类型计算出max_locals的大小

code_length和code用来存储Java源程序编译后生成的字节码指令,当虚拟机读到code中的一个字节码时,就可以对应找出这个字节码代表的指令,并且可以知道这个指令后面是否需要跟随参数,以及后续的参数应当如何解析,Code属性是u1类型,也就是可以表达2^8=256条指令,可通过查虚拟机字节码指令表来学习各个指令的作用

Code属性是Class文件中最重要的一个属性,如果Java程序中的信息分为以下两类:

  • 代码(Code,方法体里边的Java代码)
  • 元数据(Metadata,包括类,字段,方法定义以及其他信息)

那么在Class文件里,Code属性用来描述代码,其他所有信息都用来描述元数据

这里还有个有意思的地方,那就是关于Java语言中this关键字的实现,在任何实例方法里,都可以通过this关键字访问到此方法所属的对象,它的原理其实非常简单,只是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法的时候自动传入此参数而已,所以局部变量表中会预留出第一个变量槽位来存放对象实例的引用,这个处理只对实例方法有效,以及实例方法的Args_size都会是比Java语言中肉眼看到的参数个数多1

异常表属性表结构

类型 名称 数量
u2 start_pc 1
u2 end_pc 1
u2 handler_pc 1
u2 catch_type 1

异常表实际上是Java代码的一部分,尽管字节码中有最初为处理异常而设计的跳转指令,但《Java虚拟机规范》中明确要求Java语言的编译器应当选择使用异常表而不是通过跳转指令来实现Java异常及finally处理机制

当字节码从第start_pc行到第end_pc行之间(不含第end_pc行)出现了类型为catch_type或者其他之类的异常(catch_type为指向一个CONSTANT_Class_info类型常量的索引),则跳转到第handler_pc行继续进行处理,当catch_type值为0时,代表任意异常情况都需要跳转到handler_pc处进行处理

# Exceptions属性

Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是方法描述时在throws关键字后面列举出的异常

Exceptions属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

# LineNumberTable属性

LineNumberTable属性用于描述Java源代码与字节码行号(字节码的偏移量)之间的对应关系,并不是运行时必需的属性,但默认会生成到Class文件中,用于当抛出异常时,在堆栈信息中显示出错的行号,并且在调试程序的时候,按照源码行来设置断点

LineNumberTable属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

# LocalVariableTable及LocalVariableTypeTable属性

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,也不是运行时必需的属性,但默认会生成到Class文件中,用于其他人引用这个方法时,能保留参数名称,在调试期间根据参数名从上下文中获得参数值

LocalVariableTypeTable属性是JDK5引入泛型后,因为描述符中泛型的参数化类型被擦除了,用来完成泛型的描述

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

# SourceFile及SourceDebugExtension属性

SourceFile属性用于记录生成的这个Class文件的源码文件名称,对大多数类来说,类名和文件名都是一致的,但是有些特殊情况下(如内部类),需要这个属性来在堆栈中显示出错代码所属的文件名

SourceDebugExtension属性是为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容而存在的,典型的场景是在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制

SourceFile属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

SourceDebugExtension属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 debug_extension[attribute_length] 1

# ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性

  • 非static类型的变量:在实例构造器<init>()方法中进行赋值
  • static类型的变量:
    • 在类构造器<clinit>()方法中进行赋值
    • 使用ConstantValue属性进行赋值

ConstantValue属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1

# InnerClasses属性

InnerClasses属性用于记录内部类与宿主类之间的关联

InnerClasses属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_classes_info inner_classes number_of_classes

inner_classes_info表的结构

类型 名称 数量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_flags 1

inner_class_access_flags标志

标志名称 标志值 含义
ACC_PUBLIC 0x0001 内部类是否为public
ACC_PRIVATE 0x0002 内部类是否为private
ACC_PROTECTED 0x0004 内部类是否为protected
ACC_STATIC 0x0008 内部类是否为static
ACC_FINAL 0x0010 内部类是否为final
ACC_INTERFACE 0x0020 内部类是否为接口
ACC_ABSTRACT 0x0400 内部类是否为abstract
ACC_SYNTHETIC 0x1000 内部类是否并非由用户代码产生的
ACC_ANNOTATION 0x2000 内部类是不是一个注解
ACC_ENUM 0x4000 内部类是不是一个枚举

# Deprecated及Synthetic属性

Deprecated及Synthetic属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念

Deprecated属性用于表示某个类,字段或者方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用@Deprecated注解进行设置

Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,典型例子如枚举类中自动生成的枚举元素数组和嵌套类的桥接方法(Bridge Method)

Deprecated及Synthetic属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

# StackMapTable属性

StackMapTable属性在JDK6中增加,会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器

新的验证器在同样能保证Class文件合法性的前提下,省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而在编译阶段将一系列的验证类型(Verification Type)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能

StackMapTable属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

# Signature属性

Signature属性在JDK5被增加到Class文件规范中,它大幅增强了Java语言的语法,在此之后,任何类,接口,初始化方法或者成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息

之所以需要专门使用这样一个属性来记录泛型类型,是因为Java语言的泛型采用了擦除法实现的伪泛型,字节码(Code属性)中所有的泛型信息编译(类型变量,参数化类型)在编译之后都通通被擦除掉,使用擦除法的好处是实现简单(主要修改Javac编译器,虚拟机内部只做了很少的改动),非常容易实现Backport,运行期也能够节省一些类型所占用的内存空间,但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得反省信息,现在Java的反射API能够获取到的泛型信息,最终的数据来源也是这个属性

Signature属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

# BootstrapMethods属性

BootstrapMethods属性在JDK7被增加到Class文件规范中,用于保存invokedynamic指令引用的引导方法限定符

BootstrapMethods属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_bootstrap_methods 1
bootstrap_method bootstrap_methods num_bootstrap_methods

bootstrap_method结构

类型 名称 数量
u2 bootstrap_method_ref 1
u2 num_bootstrap_arguments 1
u2 bootstrap_arguments num_bootstrap_arguments

# MethodParameters属性

BootstrapMethods属性在JDK8被增加到Class文件规范中,用于记录方法的各个形参名称和信息,最初,基于存储空间的考虑,Class文件默认时不存储方法参数名称的,因为对计算机执行程序来说没有任何区别,后来在IDE中编辑使用包里边的方法时,如果只有单独的包而没有附加的JavaDoc的话,IDE是无法获得方法调用的智能提示的,这就阻碍了JAR包的传播

MethodParameters属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 parameters_count 1
parameter parameters parameters_count

parameter属性结构:

类型 名称 数量
u2 name_index 1
u2 access_flags 1

# 模块化相关属性

JDK9中提供,由于模块描述文件(module-info.java)最终是要编译成一个独立的Class文件来存储的,所以Class文件格式拓展了Module,ModulePackages,ModuleMainClass这三个属性用于支持Java模块化相关功能

Module属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 module_name_index 1
u2 module_flags 1
u2 module_version_index 1
u2 requires_count 1
require requires requires_count
u2 exports_count 1
export exports exports_count
u2 opens_count 1
open opens opens_count
u2 uses_count 1
use uses_index uses_count
u2 provides_count 1
provide provides provides_count

exports属性结构

类型 名称 数量
u2 exports_index 1
u2 exports_flags 1
u2 exports_to_count 1
export exports_to_index exports_to_count

ModulePackages属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 package_count 1
u2 package_index package_count

ModuleMainClass属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 main_class_index 1

# 运行时注解相关属性

早在JDK5时期,Java语言的语法进行了多项增强,其中之一就是提供了对注解(Annotation)的支持,到了JDK8时期,进一步加强了Java语言的注解适用范围

RuntimeVisibleAnnotations属性结构

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_annotations 1
annotation annotations num_annotations

annotation属性结构

类型 名称 数量
u2 type_index 1
u2 num_element_value_pairs 1
element_value_pair element_value_pairs num_element_value_pairs

# Class类文件结构实战

我们来通过一段简单的代码片段,尽可能多的包含了上面介绍的各种结构体,来进一步深入了解Class类文件的结构

代码清单

package org.example.helloworld.calculator;

public class Calculator implements Dividable {

    private final String errorMessage;

    public Calculator(String errorMessage) {
        this.errorMessage = errorMessage;
    }

    public double divide(double d1, double d2) {
        if (d2 == 0D) {
            throw new ArithmeticException(errorMessage);
        }

        double d;
        try {
            d = d1 / d2;
        } catch (Exception e) {
            d = Double.NaN;
        } finally {
            System.out.println("do nothing");
        }

        return d;
    }

    private class SomeInnerClass {
        private String nothing;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

javap -verbose

D:\Data\repository\local\ij\helloworld>javap -verbose target\classes\org\example\helloworld\calculator\Calculator.class
Classfile /D:/Data/repository/local/ij/helloworld/target/classes/org/example/helloworld/calculator/Calculator.class
  Last modified 2021921; size 1200 bytes
  SHA-256 checksum 065a779e61369867bb10e7ec1e84bd339f45b5a6b9bb205722b64903e73ded84
  Compiled from "Calculator.java"
public class org.example.helloworld.calculator.Calculator implements org.example.helloworld.calculator.Dividable
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #12                         // org/example/helloworld/calculator/Calculator
  super_class: #13                        // java/lang/Object
  interfaces: 1, fields: 1, methods: 2, attributes: 2
Constant pool:
   #1 = Methodref          #13.#40        // java/lang/Object."<init>":()V
   #2 = Fieldref           #12.#41        // org/example/helloworld/calculator/Calculator.errorMessage:Ljava/lang/String;
   #3 = Class              #42            // java/lang/ArithmeticException
   #4 = Methodref          #3.#43         // java/lang/ArithmeticException."<init>":(Ljava/lang/String;)V
   #5 = Fieldref           #44.#45        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = String             #46            // do nothing
   #7 = Methodref          #47.#48        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #8 = Class              #49            // java/lang/Exception
   #9 = Class              #50            // java/lang/Double
  #10 = Double             NaNd
  #12 = Class              #51            // org/example/helloworld/calculator/Calculator
  #13 = Class              #52            // java/lang/Object
  #14 = Class              #53            // org/example/helloworld/calculator/Dividable
  #15 = Class              #54            // org/example/helloworld/calculator/Calculator$SomeInnerClass
  #16 = Utf8               SomeInnerClass
  #17 = Utf8               InnerClasses
  #18 = Utf8               errorMessage
  #19 = Utf8               Ljava/lang/String;
  #20 = Utf8               <init>
  #21 = Utf8               (Ljava/lang/String;)V
  #22 = Utf8               Code
  #23 = Utf8               LineNumberTable
  #24 = Utf8               LocalVariableTable
  #25 = Utf8               this
  #26 = Utf8               Lorg/example/helloworld/calculator/Calculator;
  #27 = Utf8               divide
  #28 = Utf8               (DD)D
  #29 = Utf8               d
  #30 = Utf8               D
  #31 = Utf8               e
  #32 = Utf8               Ljava/lang/Exception;
  #33 = Utf8               d1
  #34 = Utf8               d2
  #35 = Utf8               StackMapTable
  #36 = Class              #49            // java/lang/Exception
  #37 = Class              #55            // java/lang/Throwable
  #38 = Utf8               SourceFile
  #39 = Utf8               Calculator.java
  #40 = NameAndType        #20:#56        // "<init>":()V
  #41 = NameAndType        #18:#19        // errorMessage:Ljava/lang/String;
  #42 = Utf8               java/lang/ArithmeticException
  #43 = NameAndType        #20:#21        // "<init>":(Ljava/lang/String;)V
  #44 = Class              #57            // java/lang/System
  #45 = NameAndType        #58:#59        // out:Ljava/io/PrintStream;
  #46 = Utf8               do nothing
  #47 = Class              #60            // java/io/PrintStream
  #48 = NameAndType        #61:#21        // println:(Ljava/lang/String;)V
  #49 = Utf8               java/lang/Exception
  #50 = Utf8               java/lang/Double
  #51 = Utf8               org/example/helloworld/calculator/Calculator
  #52 = Utf8               java/lang/Object
  #53 = Utf8               org/example/helloworld/calculator/Dividable
  #54 = Utf8               org/example/helloworld/calculator/Calculator$SomeInnerClass
  #55 = Utf8               java/lang/Throwable
  #56 = Utf8               ()V
  #57 = Utf8               java/lang/System
  #58 = Utf8               out
  #59 = Utf8               Ljava/io/PrintStream;
  #60 = Utf8               java/io/PrintStream
  #61 = Utf8               println
{
  public org.example.helloworld.calculator.Calculator(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #2                  // Field errorMessage:Ljava/lang/String;
         9: return
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lorg/example/helloworld/calculator/Calculator;
            0      10     1 errorMessage   Ljava/lang/String;

  public double divide(double, double);
    descriptor: (DD)D
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=9, args_size=3
         0: dload_3
         1: dconst_0
         2: dcmpl
         3: ifne          18
         6: new           #3                  // class java/lang/ArithmeticException
         9: dup
        10: aload_0
        11: getfield      #2                  // Field errorMessage:Ljava/lang/String;
        14: invokespecial #4                  // Method java/lang/ArithmeticException."<init>":(Ljava/lang/String;)V
        17: athrow
        18: dload_1
        19: dload_3
        20: ddiv
        21: dstore        5
        23: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        26: ldc           #6                  // String do nothing
        28: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        31: goto          65
        34: astore        7
        36: ldc2_w        #10                 // double NaNd
        39: dstore        5
        41: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        44: ldc           #6                  // String do nothing
        46: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        49: goto          65
        52: astore        8
        54: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        57: ldc           #6                  // String do nothing
        59: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        62: aload         8
        64: athrow
        65: dload         5
        67: dreturn
      Exception table:
         from    to  target type
            18    23    34   Class java/lang/Exception
            18    23    52   any
            34    41    52   any
            52    54    52   any
      LineNumberTable:
        line 12: 0
        line 13: 6
        line 18: 18
        line 22: 23
        line 23: 31
        line 19: 34
        line 20: 36
        line 22: 41
        line 23: 49
        line 22: 52
        line 23: 62
        line 25: 65
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           23      11     5     d   D
           36       5     7     e   Ljava/lang/Exception;
           41      11     5     d   D
            0      68     0  this   Lorg/example/helloworld/calculator/Calculator;
            0      68     1    d1   D
            0      68     3    d2   D
           65       3     5     d   D
      StackMapTable: number_of_entries = 4
        frame_type = 18 /* same */
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 81 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 252 /* append */
          offset_delta = 12
          locals = [ double ]
}
SourceFile: "Calculator.java"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171

Calculator.class.hexdump

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 	
00000000: CA FE BA BE 00 00 00 34 00 3E 0A 00 0D 00 28 09    J~:>...4.>....(.
00000010: 00 0C 00 29 07 00 2A 0A 00 03 00 2B 09 00 2C 00    ...)..*....+..,.
00000020: 2D 08 00 2E 0A 00 2F 00 30 07 00 31 07 00 32 06    -...../.0..1..2.
00000030: 7F F8 00 00 00 00 00 00 07 00 33 07 00 34 07 00    .x........3..4..
00000040: 35 07 00 36 01 00 0E 53 6F 6D 65 49 6E 6E 65 72    5..6...SomeInner
00000050: 43 6C 61 73 73 01 00 0C 49 6E 6E 65 72 43 6C 61    Class...InnerCla
00000060: 73 73 65 73 01 00 0C 65 72 72 6F 72 4D 65 73 73    sses...errorMess
00000070: 61 67 65 01 00 12 4C 6A 61 76 61 2F 6C 61 6E 67    age...Ljava/lang
00000080: 2F 53 74 72 69 6E 67 3B 01 00 06 3C 69 6E 69 74    /String;...<init
00000090: 3E 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F    >...(Ljava/lang/
000000a0: 53 74 72 69 6E 67 3B 29 56 01 00 04 43 6F 64 65    String;)V...Code
000000b0: 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62    ...LineNumberTab
000000c0: 6C 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62    le...LocalVariab
000000d0: 6C 65 54 61 62 6C 65 01 00 04 74 68 69 73 01 00    leTable...this..
000000e0: 2E 4C 6F 72 67 2F 65 78 61 6D 70 6C 65 2F 68 65    .Lorg/example/he
000000f0: 6C 6C 6F 77 6F 72 6C 64 2F 63 61 6C 63 75 6C 61    lloworld/calcula
00000100: 74 6F 72 2F 43 61 6C 63 75 6C 61 74 6F 72 3B 01    tor/Calculator;.
00000110: 00 06 64 69 76 69 64 65 01 00 05 28 44 44 29 44    ..divide...(DD)D
00000120: 01 00 01 64 01 00 01 44 01 00 01 65 01 00 15 4C    ...d...D...e...L
00000130: 6A 61 76 61 2F 6C 61 6E 67 2F 45 78 63 65 70 74    java/lang/Except
00000140: 69 6F 6E 3B 01 00 02 64 31 01 00 02 64 32 01 00    ion;...d1...d2..
00000150: 0D 53 74 61 63 6B 4D 61 70 54 61 62 6C 65 07 00    .StackMapTable..
00000160: 31 07 00 37 01 00 0A 53 6F 75 72 63 65 46 69 6C    1..7...SourceFil
00000170: 65 01 00 0F 43 61 6C 63 75 6C 61 74 6F 72 2E 6A    e...Calculator.j
00000180: 61 76 61 0C 00 14 00 38 0C 00 12 00 13 01 00 1D    ava....8........
00000190: 6A 61 76 61 2F 6C 61 6E 67 2F 41 72 69 74 68 6D    java/lang/Arithm
000001a0: 65 74 69 63 45 78 63 65 70 74 69 6F 6E 0C 00 14    eticException...
000001b0: 00 15 07 00 39 0C 00 3A 00 3B 01 00 0A 64 6F 20    ....9..:.;...do.
000001c0: 6E 6F 74 68 69 6E 67 07 00 3C 0C 00 3D 00 15 01    nothing..<..=...
000001d0: 00 13 6A 61 76 61 2F 6C 61 6E 67 2F 45 78 63 65    ..java/lang/Exce
000001e0: 70 74 69 6F 6E 01 00 10 6A 61 76 61 2F 6C 61 6E    ption...java/lan
000001f0: 67 2F 44 6F 75 62 6C 65 01 00 2C 6F 72 67 2F 65    g/Double..,org/e
00000200: 78 61 6D 70 6C 65 2F 68 65 6C 6C 6F 77 6F 72 6C    xample/helloworl
00000210: 64 2F 63 61 6C 63 75 6C 61 74 6F 72 2F 43 61 6C    d/calculator/Cal
00000220: 63 75 6C 61 74 6F 72 01 00 10 6A 61 76 61 2F 6C    culator...java/l
00000230: 61 6E 67 2F 4F 62 6A 65 63 74 01 00 2B 6F 72 67    ang/Object..+org
00000240: 2F 65 78 61 6D 70 6C 65 2F 68 65 6C 6C 6F 77 6F    /example/hellowo
00000250: 72 6C 64 2F 63 61 6C 63 75 6C 61 74 6F 72 2F 44    rld/calculator/D
00000260: 69 76 69 64 61 62 6C 65 01 00 3B 6F 72 67 2F 65    ividable..;org/e
00000270: 78 61 6D 70 6C 65 2F 68 65 6C 6C 6F 77 6F 72 6C    xample/helloworl
00000280: 64 2F 63 61 6C 63 75 6C 61 74 6F 72 2F 43 61 6C    d/calculator/Cal
00000290: 63 75 6C 61 74 6F 72 24 53 6F 6D 65 49 6E 6E 65    culator$SomeInne
000002a0: 72 43 6C 61 73 73 01 00 13 6A 61 76 61 2F 6C 61    rClass...java/la
000002b0: 6E 67 2F 54 68 72 6F 77 61 62 6C 65 01 00 03 28    ng/Throwable...(
000002c0: 29 56 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53    )V...java/lang/S
000002d0: 79 73 74 65 6D 01 00 03 6F 75 74 01 00 15 4C 6A    ystem...out...Lj
000002e0: 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65    ava/io/PrintStre
000002f0: 61 6D 3B 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72    am;...java/io/Pr
00000300: 69 6E 74 53 74 72 65 61 6D 01 00 07 70 72 69 6E    intStream...prin
00000310: 74 6C 6E 00 21 00 0C 00 0D 00 01 00 0E 00 01 00    tln.!...........
00000320: 12 00 12 00 13 00 00 00 02 00 01 00 14 00 15 00    ................
00000330: 01 00 16 00 00 00 46 00 02 00 02 00 00 00 0A 2A    ......F........*
00000340: B7 00 01 2A 2B B5 00 02 B1 00 00 00 02 00 17 00    7..*+5..1.......
00000350: 00 00 0E 00 03 00 00 00 07 00 04 00 08 00 09 00    ................
00000360: 09 00 18 00 00 00 16 00 02 00 00 00 0A 00 19 00    ................
00000370: 1A 00 00 00 00 00 0A 00 12 00 13 00 01 00 01 00    ................
00000380: 1B 00 1C 00 01 00 16 00 00 01 0B 00 04 00 09 00    ................
00000390: 00 00 44 29 0E 97 9A 00 0F BB 00 03 59 2A B4 00    ..D).....;..Y*4.
000003a0: 02 B7 00 04 BF 27 29 6F 39 05 B2 00 05 12 06 B6    .7..?')o9.2....6
000003b0: 00 07 A7 00 22 3A 07 14 00 0A 39 05 B2 00 05 12    ..'.":....9.2...
000003c0: 06 B6 00 07 A7 00 10 3A 08 B2 00 05 12 06 B6 00    .6..'..:.2....6.
000003d0: 07 19 08 BF 18 05 AF 00 04 00 12 00 17 00 22 00    ...?../.......".
000003e0: 08 00 12 00 17 00 34 00 00 00 22 00 29 00 34 00    ......4...".).4.
000003f0: 00 00 34 00 36 00 34 00 00 00 03 00 17 00 00 00    ..4.6.4.........
00000400: 32 00 0C 00 00 00 0C 00 06 00 0D 00 12 00 12 00    2...............
00000410: 17 00 16 00 1F 00 17 00 22 00 13 00 24 00 14 00    ........"...$...
00000420: 29 00 16 00 31 00 17 00 34 00 16 00 3E 00 17 00    )...1...4...>...
00000430: 41 00 19 00 18 00 00 00 48 00 07 00 17 00 0B 00    A.......H.......
00000440: 1D 00 1E 00 05 00 24 00 05 00 1F 00 20 00 07 00    ......$.........
00000450: 29 00 0B 00 1D 00 1E 00 05 00 00 00 44 00 19 00    )...........D...
00000460: 1A 00 00 00 00 00 44 00 21 00 1E 00 01 00 00 00    ......D.!.......
00000470: 44 00 22 00 1E 00 03 00 41 00 03 00 1D 00 1E 00    D.".....A.......
00000480: 05 00 23 00 00 00 0F 00 04 12 4F 07 00 24 51 07    ..#.......O..$Q.
00000490: 00 25 FC 00 0C 03 00 02 00 26 00 00 00 02 00 27    .%|......&.....'
000004a0: 00 11 00 00 00 0A 00 01 00 0F 00 0C 00 10 00 02    ................
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

# 魔数magic(u4,0x00000000-0x00000003)

首先登场的是u4的magic,值为0xCA 0xFE 0xBA 0xBE(咖啡宝贝?)

# 次版本号minor_version(u2,0x00000004-0x00000005)

紧接着的是u2的minor_version,值为0x00 0x00,表示次版本号为0.0

# 主版本号major_version(u2,0x00000006-0x00000007)

紧接着的是u2的major_version,值为0x00 0x34,表示主版本号为52.0,对应着JDK1.8

# 常量池容量constant_pool_count(u2,0x00000008-0x00000009)

紧接着的是u2的constant_pool_count,值为0x00 0x3E,表示常量池容量为62-1=61,含61个常量,原因前面有提及

# ...

以上,从二进制Class文件的hexdump中解析出信息,其实都是重复工作了,我们可以从javap -verbose的输出中看到一些较为直观的信息

大体上我们已经能够揭开Class文件结构的神秘面纱了,想要继续了解的话,可以找到字节码指令的表,并参照Java源码本身来对照学习

# 字节码指令简介

Java虚拟机的指令为一个字节长度的,由以下两部分构成

  • 代表着某种特定操作含义的数字(称为操作码,Opcode)
  • 跟随其后的零至多个代表此操作所需的参数(称为操作数,Operand)

由于Java虚拟机采用面向操作数栈而非面向寄存器的架构,所以大多数指令不包含操作数,只有一个操作码,指令的参数都存放在操作数栈中

因为限制了Java虚拟机操作码长度为一个字节,所以指令集的操作码总数不能超过2^8=256条

又因为Class文件格式放弃了编译后代码的操作数长度对齐,所以虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体数据的结构

比如要将一个16位长度的无符号整数使用两个无符号字节存储,它们的值应该是这样的:(byte1 << 8 ) | byte2

这样虽然导致解释字节码时会损失一些性能,但是带来的优点是可以获得尽可能精简的编译代码,以及更高的传输效率,这是因为当初Java语言设计之初主要面向网络,智能家电的技术背景所决定的

不考虑异常处理的话,Java虚拟机的解释器大致就是按照以下的伪代码执行模型来处理字节码的

do {
    自动计算PC寄存器的值加1;
    根据PC寄存器指示的位置,从字节码流中取出操作码;
    if (字节码存在操作数) 从字节码流中取出操作数;
    执行操作码所定义的操作;
} while (字节码流长度 > 0);
1
2
3
4
5
6

# 字节码与数据类型

在Java虚拟机指令集中,大多数指令都包含其操作所对应的数据类型信息,比如iload指令用于从局部变量表中加载int类型数据到操作数栈中,而fload指令加载的则是float类型数据,这两条指令的操作在虚拟机内部可能是由同一段代码来实现的,但是在Class文件中它们必须拥有各自独立的操作码

下表列举了Java虚拟机所支持的与数据类型相关的字节码指令,通过使用数据类型列所代表的特殊字符替换opcode列的指令模板中的T,就可以得到具体的字节码指令

opcode byte short int long float double char reference
Tipush bipush sipush
Tconst iconst lconst fconst dconst aconst
Tload iload lload fload dload aload
Tstore istore lstore fstore dstore astore
Tinc iinc
Taload baload saload iaload laload faload daload caload aaload
Tastore bastore sastore iastore lastore fastore dastore castore aastore
Tadd iadd ladd fadd dadd
Tsub isub lsub fsub dsub
Tmul imul lmul fmul dmul
Tdiv idiv ldiv fdiv ddiv
Trem irem lrem frem drem
Tneg ineg lneg fneg dneg
Tshl ishl lshl
Tshr ishr lshr
Tushr iushr lushr
Tand iand land
Tor ior lor
Txor ixor lxor
i2T i2b i2s i2l i2f i2d
l2T l2i l2f l2d
f2T f2i f2l f2d
d2T d2i d2l d2f
Tcmp lcmp
Tcmpl fcmpl dcmpl
Tcmpg fcmpg dcmpg
if_TcmpOP if_icmpOP if_acmpOP
Treturn ireturn lreturn freturn dreturn areturn

# 加载和存储指令

加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,包括:

  • 将一个局部变量加载到操作栈:Tload
  • 将一个数值从操作数栈存储到局部变量表:Tstore
  • 将一个常量加载到操作数栈:Tipush,ldc*,Tconst
  • 扩充局部变量表的访问索引的指令:wide

# 运算指令

运算指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶

  • 加法指令:Tadd
  • 减法指令:Tsub
  • 乘法指令:Tmul
  • 除法指令:Tdiv
  • 求余指令:Trem
  • 取反指令:Tneg
  • 位移指令:Tshl,Tshr
  • 按位或指令:Tor
  • 按位与指令:Tand
  • 按位异或指令:Txor
  • 局部变量自增指令:Tinc
  • 比较指令:Tcmp,Tcmpl,Tcmpg

Java虚拟机在实现处理整数时,只有除法指令(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为零时会导致虚拟机抛出ArithmeticException,其余任何整型运算场景都不应该抛出运行时异常。在实现处理浮点数时,严格遵循IEEE 754中的规范,所有的运算结果都必须舍入到适当的精度

# 类型转换指令

类型转换指令可以将两种不同的数值类型互相转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用来处理字节码指令集中数据类型相关指令无法与数据类型一一对应的问题

Java虚拟机直接支持以下数字类型的宽化类型转换(Widening Numeric Conversion,即小范围类型向大范围类型的安全转换)

  • int类型到long,float,double类型
  • long类型到float,double类型
  • float类型到double类型

与之相对的,处理窄化类型转换(Narrowing Numeric Conversion)时,就必须显式地使用转换指令来完成

  • int转:i2T
  • long转:l2T
  • float转:f2T
  • double转:d2T

窄化类型转换可能会导致转换结果产生不同的正负号,不同的数量级的情况,也可能导致数值的精度丢失,但却永远不可能导致虚拟机抛出运行时异常

# 对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令,这些指令包括:

  • 创建类实例的指令:new
  • 创建数组的指令:newarray,anewarray,multianewarray
  • 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者称为实例变量)的指令:getfield,putfield,getstatic,putstatic
  • 把一个数组元素加载到操作数栈的指令:Taload
  • 将一个操作数栈的值存储到数组元素中的指令:Tastore
  • 取数组长度的指令:arraylength
  • 检查类实例类型的指令:instanceof,checkcast

# 操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

  • 将操作数栈的栈顶一个或两个元素出栈:pop,pop2
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup,dup2,dup_x1,dup2_x1,dup_x2,dup2_x2
  • 将栈最顶端的两个数值互换:swap

# 控制转移指令

控制转移指令可以让Java虚拟机有条件或无条件地从指定位置指令(而不是控制转移指令)的下一条指令继续执行程序,从概念模型上理解,可以认为控制指令就是在有条件或无条件地修改PC寄存器的值,指令包括:

  • 条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull,if_icmpeq,if_icmpne,if_icmplt,if_icmpgt,if_icmple,if_icmpge,if_acmpeq,if_acmpne
  • 复合条件分支:tableswitch,lookupswitch
  • 无条件分支:goto,goto_w,jsr,jsr_w,ret

# 方法调用和返回指令

指令包括:

  • invokevirtual指令:用于调用对象的实例方法
  • invokeinterface指令:用于调用接口方法
  • invokespecial指令:用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和父类方法
  • invokestatic指令:用于调用类静态方法
  • invokedynamic指令:用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前面四条指令的分派逻辑都固化在Java虚拟机内部,用户无法改变,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的

# 异常处理指令

在Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现,而处理异常(catch语句)不是由字节码指令来实现的(很久之前曾经使用jsr和ret指令来实现,现在已经废弃了),而是采用异常表来完成

# 同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor,更常见的是直接将它称为“锁”)来实现的

方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作之中,虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否被声明为同步方法

当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程Monitor,然后才能执行方法,最后当方法完成(无论正常完成还是非正常完成)时释放管程,在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程

如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法边界之外时自动释放

同步一段指令集序列通常是由Java语言中的synchronized语句块来表示的,Java虚拟机的指令集中有两条指令来支持synchronized关键字的语义:

  • monitorenter
  • monitorexit

编译器必须确保无论方法通过何种方式完成,方法中调用的每条monitorenter指令都必须有其对应的monitorexit指令,无论这个方法是正常结束还是异常结束

最近更新: 9/25/2021, 3:21:26 PM