声明:本文基于《深入理解Java虚拟机》一书,可以看作是该书的读书笔记
Class类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各种数据项目严格按照顺序紧凑的排列在Class文件中,没有任何分隔符,没有空隙存在。当遇到需要占用8位字节以上空间的数据项是,则会按照高位在前(Big-Endian)的方式分割成若干个8位字节进行存储。
Class文件格式采用类似C语言结构体的伪结构来存储数据,只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数用来描述数字、索引引用、数量值或按UTF-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个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文件的版本
每个Class文件的头4个字节称为魔数(MagicNumber),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别。
紧接着魔数的4个字节存储的是Class文件的版本号:第5/6个字节是次版本(MinorVersion)号,第7/8个字节是主版本(Major Version)号。Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容低版本的JDK。
常量池
紧接着主版本号的就是常量池,常量池可以理解为class文件的资源仓库,它是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,也是class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量不是固定的,所以常量池入口需要放置一项u2类型的数据,代表常量池中的容量计数。不过,这里需要注意的是,这个容器计数是从1开始的而不是从0开始,也就是说,常量池中常量的个数是这个容器计数-1。将0空出来的目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。class文件中只有常量池的容量计数是从1开始的,对于其它集合类型,比如接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言的常量概念,如文本字符串、声明为final的常量等。而符号引用则属于编译原理方面的概念,它包括三方面的内容:
类和接口的全限定名(Fully Qualified Name)
字段的名称和描述符(Descriptor)
方法的名称和描述符
Java代码在进行javac编译的时候并不像C和C++那样有连接这一步,而是在虚拟机加载class文件的时候进行动态连接。也就是说,在class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,虚拟机也就无法使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
常量池中的每一项都是一个表,在JDK1.7之前有11中结构不同的表结构,在JDK1.7中为了更好的支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。
这14个表的开始第一个字节是一个u1类型的tag,用来标识是哪一种常量类型。这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_InterfaceMethod_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
javap –verbose .class文件名(不带.class)命令可以快速的计算出class文件结构的内容。
14种常量项的结构:
常量 | 项目 | 类型 | 含义 |
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_InterfaceMethod_info | tag | U1 | 11 |
index | U2 | 指向声明方法的接口描述符COSNTANT_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 | U2 | 值必须在1-9之间,决定了方法句柄的类型,方法句柄累心的值表示方法句柄的字节码行为 | |
reference_ index | U2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info | tag | U1 | 16 |
descriptor_index | U2 | 值必须是对常量池的有效索引,常量池在改索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符 | |
CONSTANT_InvokeDynamic_info | tag | U1 | 18 |
bootstrap_method_attrindex | U2 | 值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | U2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是COSTANT_NameAndType_info结构,表示方法名和方法描述符 |
访问标志
常量池结束后紧接着的两个字节代表访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类还是接口;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。具体的标志位以及含义如下表:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否是public |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真 |
ACC_INTERFACE | 0x0200 | 标识是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否是abstract,对于接口和抽象类来说为真,其他类都为假 |
ACC_SYNITHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类 |
由于access_flags是两个字节大小,一共有十六个标志位可以使用,当前仅仅定义了8个,没有用到的标志位都是0。对于一个类来说,可能会有多个访问标志,这时就可以对照上表中的标志值取或运算的值。
类索引、父类索引和接口索引集合
在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来确定类的继承关系。由于Java中是单继承,所以父类索引只有一个,java.lang.Object为0;但Java类可以实现多个接口,所以接口索引是一个集合。
类索引用来确定这个类的全限定名,这个全限定名就是说一个类的类名包含所有的包名,然后使用"/"代替"."。接口索引集合存储了implements语句后面按照从左到右的顺序的接口。
类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量。然后再CONSTANT_Class_info常量中的索引就可以找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名。对于接口索引集合,入口的第一项——u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
字段表集合
字段表(field_info)用来描述接口或类中声明的变量。字段(field)包括类级变量和实例级变量,但不包括方法内部的局部变量。
类型 | 名称 | 数量 |
U2 | access_flags | 1 |
U2 | name_index | 1 |
U2 | descriptor_index | 1 |
U2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
其中的字段修饰符access_flags,和类中的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 | 字段是否是enum |
ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED只能选择一个,ACC_FINAL和ACC_VOLATILE不能同时选择。接口中的字段必须有ACC_PUBLIC、ACC_STATIC和ACC_FINAL标志,这是Java语言本身的规则决定的。
access_flags给出了字段中所有可以用布尔值表示的修饰符,剩下的信息就是字段的名字、变量类型等信息。access_flags后面的是name_index和descriptor_index,前者是字段名的常量池索引,后者是字段描述符的常量池索引。name_index可以描述字段的名字,descriptor_index可以描述字段的数据类型。不过,对于方法的描述符来说就要复杂一些,因为一个方法除了返回值类型,还有参数类型,而且参数的个数还不确定。根据描述符规则,这些类型都使用一个大写字母来表示,如下表:
标识字符 | 含义 | 标识字符 | 含义 |
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",一个double数组"double[]"将标记为"[D"。
当描述符用来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号"()"内。比如方法void inc()的描述符是:()V。方法java.lang.String toString()的描述符是:()Ljava/lang/String。方法intindexOf(char[] source,int sourceOffset,int sourceCount,char[] target,inttargetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I。
字段表集合中不会列出从父类或接口中继承来的字段,但有可能会出现原本Java程序中没有的字段。比较典型的例子是内部类,为了在内部类中保持对外部类的访问性,会增加一个指向外部类实例的字段。另外,在Java语言中字段无法重载,也就是字段名不能重复,即使两个字段的数据类型、修饰符都不相同。不过对于字节码来说,如果两个字段的描述符不一致,那么就可以有重复的字段名。
方法表集合
在字段表集合中介绍了字段的描述符和方法的描述符,对于理解方法表有很大帮助。class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同,这里就不再列出。不过,方法表的访问标志和字段的不同,列出如下:
标识名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 方法是否是public |
ACC_PRIVATE | 0x0002 | 方法是否是private |
ACC_PUBLICPROTECTED | 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 | 方法是否是由编译器自动产生的 |
在Java中,要重载一个方法,除了要与原方法具有相同的方法名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是特征签名只包含参数个数和类型,并不包含返回值类型,所以Java语言中是无法仅仅依靠返回值的不同来对一个方法重载的。但是在class文件格式中,特征签名还包括返回值类型,也就是说只有返回值类型不同的两个方法也可以存在。
属性表集合
在class文件、字段表和方法表都可以携带自己的属性表集合,来描述某些场景专有的信息。
与class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制比较少,不要求严格的顺序,只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机会在运行时忽略掉那些不认识的信息。
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译后的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exception | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或匿名类时才能拥有这个属性,来标识这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | 供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型检查 |
Signature | 类、方法表、字段表 | 用于保存泛型中的类型信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,为了描述泛型参数化类型 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 类、方法表、字段表 | 与RuntimeVisibleAnnotations作用相反 |
RuntimeVisbleParameterAnnotations | 方法表 | 与RuntimeVisibleAnnotations类似 |
RuntimeInvisbleParameterAnnotations | 方法表 | 与RuntimeInvisibleAnnotations类似 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokedynamic指令引用的引导方法限定符 |
属性表集合存在的位置是不确定的,不仅可以存储在class文件结尾处,还可以作为数据项存在于类、方法表集合和字段表集合中。对于存在于class类文件中的属性表集合很好理解,毕竟在开头的class文件结构图中的最后一部分就是属性表集合,这时属性表集合作为构成class文件结构的一个大部分。剩下的存在于类中、方法表集合与字段表集合中的属性表集合,其实是作为它们的一个数据项存在的。
存在于类中的属性表集合,存储了关于这个类的一些信息。比如这个类是否是过时的(Deprecated)、在泛型中保存类的类型参数(由于生成class文件后会进行类型擦除,Java中的泛型是一种伪泛型)和动态注解等信息;存放在方法表集合中的属性表集合存储了关于方法的信息,最主要的就是Code属性,存储了字节码指令;存放于字段表集合中的属性表集合存储了关于字段的信息,我们这里的例子没有涉及到字段的属性,不过当在类中定义了静态常量(static final)并且这个常量有初始值时会将这个值作为属性存储在字段表中的属性表集合中。
由于属性表集合的限制较小,每个属性都会有自己的格式,因此class文件对于属性的格式要求也比较宽松,只需要满足一些特定的条件即可。下表是属性的结构:
类型 | 名称 | 数量 |
U2 | attribute_name_index | 1 |
U4 | attribute_length | 1 |
U1 | info | attribute_length |
可以看出,class文件规定的属性格式只有前6个字节:两个字节的属性名称的索引和4个字节的属性长度,接下来就要按照这个长度存储属性值了。这样的宽松格式使得属性表的结构可以多样变化,甚至可以在属性的内容中再加入一个属性,比较常用的就是方法表集合中的Code属性,在Code属性中还有LineNumberTable属性和LocalVariableTable属性等。





