Class 文件格式
最后更新于
这有帮助吗?
最后更新于
这有帮助吗?
类或接口不一定定义在文件中,也可以由类加载器生成。
class 文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在 class 文件中,中间没有添加任何分隔符。对于占用8位字节以上的数据,则按照高位在前(地址低位)的方式分割成多个字节存储。
根据 Java 虚拟机规范,class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种结构只包含两种数据:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别表示1、2、4、8个字节的无符号数,可以用来描述数字、符号引用、数量值或者按照UTF-8编码构成字符串值。
表是由多个无符号数或者其他表组成的复合数据类型,所有的表都以"_info"结尾,表用于描述有层次关系的复合结构的数据,整个class文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但是数量不定的多个数据时,经常会使用一个前置的容量计数器加若干连续数据项表示,这时称这些连续的数据为某一类型的集合。
每个 class 文件的前4个字节成为魔数,它的唯一作用就是确定这个文件是否为一个能被虚拟机接收的 class 文件。
class 文件的魔数是为:0xCAFEBABE
class 文件的第5、6个字节为次版本号(minor version),第7、8个字节为主版本号(major version)。Java 的版本号从 45 开始,每发布一个大版本就加1。高版本的 JDK 能向下兼容低版本的class文件,但不能运行更高版本的class,虚拟机必须拒绝执行高于其版本号的class文件。
常量池可以理解为 class 的资源仓库,它是class文件文件结构中与其他项目关联最多的数据类型,也是占用class 文件空间最大的数据项目之一。
由于常量池中常量的数量不是固定的,因此需要在常量池的入口放置一个u2类型的数据代表常量池容量计数值(constant_pool_count),这个容器计数从1开始而不是从0开始。如果常量池中有10个常量,那么该值为11(索引范围为1-10)。
常量池中主要存放两大类常量:
字面量:文本字符串、final 常量值等
符号引用
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
Java 代码在进行 javac 编译时,并不像c/c++那样有「连接」这一步,而是在虚拟机加载class文件时进行动态连接。也就是说 class文件不会保存方法、字段的内存布局信息,因此这些字段、方法的符号引用不经过运行期转换无法获得真正的内存地址入口,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
常量池的每一个常量都是一个表,JDK 1.7 开始已经有14种不同类型的表结构。这些表结构的共同点是开始的第一位是一个u1类型的标志位(tag),代表当前这个常量属于哪种类型。
常量池后的两个字节代表访问标识(access_flag),用于识别类或接口层次的访问信息,包括:这个class 是类还是接口、是否定义为public、是否定义为 abstract、是否定义为 final 。
类索引、父类索引和接口索引按顺序排在访问标识后,
类索引(this_class)与父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合是一组u2类型的数据的集合,class 文件由这三项数据确定类的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合确定这个类实现了哪些接口,实现接口按照implements语句的顺序从左到右排列。
类索引和父类索引指向一个类型为CONSTANT_Class_info的类描述符常量,对于接口集合,入口的第一项u2类型的数据为接口计数器,表示索引表的容量,剩下的为u2类型的数据,指向类描述常量。
字段表用于描述类或接口中声明的变量,字段包括类级变量和实际级变量,但不包括方法内部声明的局部变量。字段表结构如下表:
类型
名称
数量
u2
access_flag
1
u2
name_index
1
u2
descriptor_index
1
u2
attributes_count
1
attribute_info
attributes
attributes_count
字段修饰符放在 access_flags
项目中,其可以设置的标识位如下表所示:
标志名称
标志值
含义
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
是否是枚举
name_index 和 descriptor_index 是对常量的引用,分别代表字段的简单名称以及字段和方法的描述符。
全限定名称:org/test/class/TestClass
简单名称:没有类型或参数修饰的方法或字段名称,int m;
其中 m 的简单名称是"m".
描述符:描述字段的数据类型、方法的参数列表和返回值。根据描述符规则,基本数据类型以及代表无返回值的void类型都用一个大写字母来表示,而对象类型则用字符L加对象的全限定名表示,如下表:
标识字符
含义
B
基本类型 byte
C
基本类型 char
D
基本类型 double
F
基本类型 float
I
基本类型 int
J
基本类型 long
S
基本类型 short
Z
基本类型 boolean
V
特殊类型 void
L
对象类型,如Ljava/lang/Object
对于数组类型,每个维度使用一个前置的"["来描述,如定义为"java.lang.String[][]"的二维数组,将被记录为"[[Ljava/lang/String;"。
用描述符描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号内。如方法"inc()"的描述符为"()V",方法"int add(int a,int b)"的表述符为"(II)I"。
字段表集合中不会列出从超类或者父接口中继承的字段,但有可能列出原本java代码中不包含的字段,比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表集合与字段表集合结构相同,而由于 volatile 和 transient 不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT,而增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT。方法表的标志位取值见下表:
标志名称
标志值
含义
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_STRICTFP
0x0800
是否 strictfp
ACC_SYNTHETIC
0x1000
是否由编译器自动生成
方法的代码经过编译成字节码指令后,存放在方法属性表集合中一个名为"Code"的属性里。
如果父类方法没有在子类中被重写(override),那么方法表集合中不会出现父类的方法,但可能出现有编译器添加的方法,例如类构造器"<clinit>"和实例构造器"<init>"。
在 Java 语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必有有一个与原方法不同的特征签名,特征签名就是一个方法中年各个参数在常量池中的字段符合引用的集合,即返回值不包含在特征签名里,因此Java中不能以返回值不同而对方法进行重载。但在 class 文件格式中,特征签名的范围更大,只要描述符不完全一致的两个方法都可以共存,也就是说,如果两个方法具有相同的名称和特征签名(java语言层面),但是返回值不同,是可以合法共存于一个class文件中。
Java 代码层面的方法特征签名包含方法名称、参数顺序和参数类型,而字节码层面的方法特征签名还包括返回值和受检异常表。
属性表用于描述某些场景专有的信息,在 Class文件、字段表、方法表都可以携带自己的属性表集合。属性表集合的限制比较宽松,不要求各个属性表有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己的属性信息,Java 虚拟机在运行时会忽略掉它不认识的属性。
Java 虚拟机规范(SE 7)中预定义的属性如下表:
属性表结构:
Code 属性出现咋方法表的属性集合中,但并非所有方法都必须存在 Code 属性,比如接口或抽象类中的抽象方法。
Code 属性表结构如下表:
attribute_name_index 是一项指向CONSTANT_Utf8_info 型常量的索引,常量值固定为 "Code",代表该属性的属性名称。attribute_length 代表了属性值的长度。
max_stack 代表了操作数栈深度的最大值,在方法执行的任何时刻,操作数栈都不会超过这个深度,虚拟机运行的时候要根据这个值来分配栈帧中操作栈深度。
max_locals 代表了局部变量表所需的存储空间,单位是slot。Slot是虚拟机为局部变量分配内存时使用的最小单位,对于byte、short、int等长度不超过32位的数据类型,每个数据类型占用1个 Slot,对于 long/double 需要两个 Slot 来存放。方法参数(包括实例方法的隐藏参数"this")、显示异常处理的参数(try-catch块中catch块定义的异常)、方法体中定义的局部变量都需要使用局部变量表来保存。另外,max_locals并不等于所有局部变量所占slot之和,因为局部变量表的 slot 可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占用的slot可以被其他局部变量使用。
code_length 和 code 用于存放Java源程序编译后的字节码指令,code_length 代表字节码的长度,code 是存储字节码指令的一系列字节流,每个指令都是一个u1类型的单字节。
方法表中与 Code 属性平级的属性,列举出方法中可能抛出的受检查异常,也就是 throws 关键字后面列举的异常。
描述 Java 源代码行数与字节码行数(偏移量)之间的对应关系,不是运行时必须的属性。如果不生成 LineNumberTable 属性,则运行时抛出异常时,堆栈中不会显示对应的行数,并且在调试程序时,无法按照源码行设置断点。
用于描述栈帧中局部变量表中的变量与Java源代码中定义的变量的对应关系,不是运行时必须的属性。如果没有生成,则引用该方法时定义的参数名称将会消失,IDE将会使用arg0、arg1等名字来替换。
用于记录生成这个 Class 文件的源码文件名称,这个属性也是可选的。
这个属性的作用是通知虚拟机自动为静态变量赋值,只有被 static 修饰的变量才可以使用这项属性。
对于非静态变量的赋值是在构造器<init>中进行,对于类变量,可以使用ConstantValue属性或者在类初始化<clinit>方法中进行。目前 Sun javac 编译器的选择是:如果同时使用final 和static来修饰一个变量,并且这个变量的类型是基本类型或String,就生成 ConstantValue 属性初始化,否则就在<clinit>中进行初始化。
InnerClass 属性记录内部类与宿主类之间的关联,如果一个类中定义了内部类,那么编译器会为它以及它所包含的内部类生成InnerClass属性。
这两个属性都是标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。
Deprecated 表示类、字段或方法已经不再推荐使用,可通过@deprecated注解设置。
Synthetic 代表字段或方法不是由Java源代码生成的 ,而是由编译器自行添加的。
这个属性在 JDK 1.6发布后添加到Class文件规范中,是一个复杂的变长属性,位于 Code 属性的属性表中,这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于替代以前比较消耗性能的基于数据流分析的类型推导验证器。
JDK 1.5 发布后添加到Class文件规范中,是一个可选的定长属性,可以出现在类、属性表和方法表结构的属性表中。主要用于记录类、接口、初始化方法或成员的泛型签名信息,可用于运行时通过反射获取泛型类型。
在 JDK 1.7 发布后增加到了 Class 文件规范之中,是一个复杂的变长属性,位于类文件的属性表中,这个属性用于保存 invokedynamic 指令引用的引导方法限定符。Java 虚拟机规范 SE 7 规定,如果某个类文件结构的常量池中曾经出现过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的 BootstrapMethods 属性。