Class文件结构详解
引言
众所周知,Java语言有一个很重要的特点是平台无关性,即用Java语言编写的程序可以在不同平台之间无缝迁移,Java对这个特性有一个著名的宣传口号:“一次编写,到处运行(Write Once,Run AnyWhere)”。Java能够实现平台无关性的原因是它在平台之上提供了一个Java运行环境,也就是JVM。在Java虚拟机上开发出了许多语言,包括Clojure、Groovy、JRuby、Jython、Scala等。Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件关联,Class文件中包含了Java虚拟机指令集和符号表以及其他辅助信息,虚拟机并不关心Class来源于何种语言。
1.Class文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。根据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 |
2.各字段详细解释
2.1 magic 魔数
每个Class文件的头四个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件。很多文件存储标准中都使用魔术来进行身份识别,譬如图片格式,如gir或者jpeg等在文件头中都存在魔数。文件格式的制定者可以自由地选择魔数值,只要这个值还没有被广泛采用过同时又不会引起混淆即可。Class文件的魔数值为0xCAFEBABE(咖啡宝贝)
2.2 major.minor 版本号
第五六个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。如果版本号不对,会引起 Unsupported major.minor version xx.xx 的异常错误。
在 Sun 的 JDK 1.0.2 发布版中,JVM 实现支持从 45.0 到 45.3 的 class 文件格式。在所有 JDK 1.1 发布版中的 JVM 都能够支持版本从 45.0 到 45.65535 的 class 文件格式。在 Sun 的 1.2 版本的 SDK 中,JVM 能够支持从版本 45.0 到46.0 的 class 文件格式。
我们可以得到的信息就是每个版本的 JDK 编译器编译出的 class 文件中都带有一个版本号,不同的 JVM 能接受一个范围 class 版本号,超出范围则要出错。不过一般都是能向后兼容的,知道 Sun 在做 Solaris 的一句口号吗?保持对先前版本的 100% 二进制兼容性,这也是对客户的投资保护。
使用javac编译时,带上-target xx -source xx可以指定编译的jdk版本
2.3 constant_pool 常量池
紧接着版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据项,也是占用Class文件空间最大的数据项目之一。
由于常量池中的常量数量不固定,所以在常量池的入口需要放置一项u2类型的数据,代表容量池容量计数值(constant_pool_count),这个容量计数从1而不是0开始。
constant_pool_count:占2字节,0x0016,转化为十进制为22,即说明常量池中有21个常量(只有常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~22。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示。
常量池中主要存放两大类常量:字面量(Literal)和 符号引用(Symbolic Reference)。字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量:
-
类和接口的全限定名
-
字段的名称和描述符
-
方法的名称和描述符
常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。一个常量池中的每个常量项都逃不脱这14种结构。根据下图每个类型的描述我们也可以知道每个类型是用来描述常量池中哪些内容(主要是字面量、符号引用)的。比如:CONSTANT_Integer_info是用来描述常量池中字面量信息的,而且只是整型字面量信息。而标志为15、16、18的常量项类型是用来支持动态语言调用的(jdk1.7时才加入的)。
14种常量类型代表的具体含义见下表:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点形字面量 |
CONSTANT_Long_info | 5 | 长整形字面量 |
CONSTANT_Double_info | 6 | 双精度浮点形字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
14种常量项的结构总表:
WechatIMG20.png WechatIMG21.png WechatIMG22.png2.4 使用javap命令分析class文件
javap分析class文件用法:javap -verbose class文件名
$ javap -verbose TestClass.class
Classfile /E:/studytry/com/zlcook/clazz/TestClass.class
Last modified 2017-4-7; size 292 bytes
MD5 checksum 486567c6d4d7432fc359230fed9c92c7
Compiled from "TestClass.java"
public class com.zlcook.clazz.TestClass
SourceFile: "TestClass.java"
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/zlcook/clazz/TestClass.m:I
#3 = Class #17 // com/zlcook/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/zlcook/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public com.zlcook.clazz.TestClass();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
public int inc();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
}
上面通过javap命令得到的结果,该结果显示的很友好,由过上面的理论我们可以很清楚的看到常量池一共18项。
2.5 access_flags 访问标志
常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是接口还是类;是否被定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
访问标志如下表所示:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令,JDK1.2以后编译出来的类这个标志为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口和抽象类,此标志为真,其它类为假 |
ACC_SYNTHETIC | 0x1000 | 标识别这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
根据上面的表格,测试类的访问标志0x0021= 0x0001 | 0x0020 =ACC_PUBLIC | ACC_SUPER
2.6 this_class 类索引、super_class 父类索引和interfaces 接口索引集合
Class文件中由这3项数据来确定这个类的继承关系
-
this_class:类索引,用于确定这个类的全限定名,占2字节
-
super_class:父类索引,用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
-
interfaces_count:接口索引计数器,占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节
-
interfaces:接口索引集合,一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中
this_class、super_class与interfaces中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
2.7 fields 字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,但不包括方法内部声明的局部变量。Java中描述一个字段可以包括的信息有:字段的作用域(public、protectded、private修饰符)、实例变量还是类变量(static修饰符)、可变性(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代表字段修饰符,name_index和descriptor_index都是对常量池的引用,分表代表字段的简单名称和字段的描述符。字段的简单名称是指没有类型的字段名称,而字段描述符则是用来描述字段的数据类型。根据描述符规则,基本数据类型以及void类型(修饰方法返回值,后面会介绍)都用一个大写字符表示,而对象类型用字符L加对象的全限定名来表示。详见下表:
标识符 | 含义 | 标识符 | 含义 |
---|---|---|---|
B | 基本类型byte | J | 基本类型long |
C | 基本类型char | S | 基本类型short |
D | 基本类型double | Z | 基本类型boolean |
F | 基本类型float | V | 特殊类型void |
I | 基本类型int | L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一维度将使用一个前置的“[”字符描述,如一个定义为java.lang.String[][]类型的二维数组,将被记录为“[[Ljava/lang/String;”,一个整型数组将被记录为“[I"。
紧跟在descriptor_index之后的是一个属性表集合,字段都可以在属性表中描述零至多项的额外信息。attribute_count代表属性表计数器,如果字段没有额外信息,那么其值为0。如果其值不为0,则attribute_count后面会紧跟着attribute_count个attribute数据项。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类的实例字段。
2.8 methods 方法表集合
见名知意,方法表用来描述类或接口中的方法。它的结构和字段表一致。
由于ACC_VOLATILE标志和ACC_TRANSIENT标志不能修饰方法,所以access_flags中不包含这两项,同时增加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 | 字段是否为编译器自动产生 |
需要注意的是与字段描述符不同,方法描述符需要描述方法的参数列表和返回值,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。返回值的描述规则与描述字段的数据类型一样。举个例子,方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String;”,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CIII)I”。
至此,方法的定义已经通过访问标志,名称索引,描述符索引表达清楚了,但是方法里的代码去哪了?这就要轮到属性表集合登场了。方法里的Java代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性表中。属性表作为class文件格式中最具有扩展性的一种数据项目,将在随后介绍。
与字段表集合相对应的,子类的方法表集合中不会出现继承自父类的方法信息,但同样的,有可能出现编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。
在Java语言中,重载一个方法除了要求和原方法拥有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名(方法参数集合),由于特征签名不包含返回值,故Java语言中不能仅仅依靠返回值的不同对一个已有的方法重载;但是在Class文件格式中,特征签名即为方法描述符,只要是描述符不完全相同的2个方法也可以合法共存,即2个除了返回值不同之外完全相同的方法在Class文件中也可以合法共存.
2.9 attributes 属性表集合
属性表(attribute_info)在前面的讲解中已经出现过多次了,字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格的顺序。为了能够正确解析Class文件,《Java虚拟机规范(Java SE 7)》中,预定义了21项属性表。下文将对一些常用的属性表进行讲解。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源代码行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过JAVA堆栈来定位到JSP文件的行号,JSR-45规范为这些非JAVA语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | JDK1.5中新增的属性,为动态注解提供支持,RuntimeVisibleAnnotations用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | JDK1.5中新增的属性,与RuntimeVisibleAnnotations作用刚好相反,用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | JDK1.5中新增的属性,与RuntimeVisibleAnnotations作用类似,只不过作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | JDK1.5中新增的属性,与RuntimeInvisibleAnnotations作用类似,只不过作用对象为方法参数 |
AnnotationDefault | 方法表 | JDK1.5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | JDK1.7中新增的属性,永不保存invokedynamic指令引用的引导方法限定符 |
对于每个属性表,它的名称需要从常量池中引用给一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则时完全自定义的,值需要通过一个u4的长度属性说明属性值所占用的位数即可。一个符合规则的属性表应该满足下表所定义的结构。
类型 | 名称 | 数量 |
---|---|---|
u2 | attrbute_name_index | 1 |
u4 | attrbute_length | 1 |
u1 | info | attrbute_length |
2.9.1 Code属性
Code属性存储编译后的字节码指令,它出现在方法表的属性集合中,但并非所有方法都必须存在这个属性,譬如抽象方法就不存在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 |
-
max_stack:操作数栈深度最大值,在方法执行的任何时刻,操作数栈深度都不会超过这个值。虚拟机运行时根据这个值来分配栈帧的操作数栈深度
-
max_locals:局部变量表所需存储空间,单位为Slot(参见备注四)。并不是所有局部变量占用的Slot之和,当一个局部变量的生命周期结束后,其所占用的Slot将分配给其它依然存活的局部变量使用,按此方式计算出方法运行时局部变量表所需的存储空间
-
code_length和code:用来存储字节码指令,cond_length代表字节码长度,code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个u1类型的单字节,当虚拟机读到code中的一个字节码时,就可以找出这个字节码代表的是什么指令,并且可以知道这条指令后面是否需要紧随参数,以及参数应当如何理解。u1类型的取值范围是0x00~0xFF,对应十进制的0 ~ 255,也就是一共可以表达256条指令,目前Java虚拟机规范已经定义了200条编码值对应的指令含义,编码与指令之间的对应关系可以查阅“虚拟机字节码指令表”。
Code属性是class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个class文件中,Code属性用于描述代码,所有的其他数据项都用于描述元数据。
2.9.2 ConstantValue属性
ConstantValue属性:通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用这项属性,其结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
可以看出ConstantValue属性是一个定长属性,其中attribute_length的值固定为0x00000002,constantvalue_index为一常量池字面量类型常量索引(Class文件格式的常量类型中只有与基本类型和字符串类型相对应的字面量常量,所以ConstantValue属性只支持基本类型和字符串类型)
对非static类型变量(实例变量,如:int a = 123;)的赋值是在实例构造器<init>方法中进行的
对类变量(如:static int a = 123;)的赋值有2种选择,在类构造器<clinit>方法中或使用ConstantValue属性。当前Javac编译器的选择是:如果变量同时被static和final修饰(虚拟机规范只要求有ConstantValue属性的字段必须设置ACC_STATIC标志,对final关键字的要求是Javac编译器自己加入的要求),并且该变量的数据类型为基本类型或字符串类型,就生成ConstantValue属性进行初始化;否则在类构造器<clinit>方法中进行初始化.
其他属性不再一一介绍,感兴趣的读者可自行查找资料或查看参考连接
参考: