当我们使用命令行来运行Java程序时,会先使用javac将java文件编译成class文件,然后使用java命令运行class文件。那么有没有思考过class文件的作用与结构呢?
Java语言在诞生之初相较于其他语言来讲一个很大的竞争力就是Java是一个跨平台的语言,可移植性极好。诚然C语言与C++的可移植性也很不错,然而C/C++编写的程序往往依赖系统提供的API,并且在很多实现细节上没有严格规定,这使得即使将源代码在新的平台重新进行编译可能也无法使用。而Java则不存在这样的问题,Java具有“一次编写,到处运行的特点”,一方面,Java使用虚拟机来运行Java程序,向上屏蔽了硬件与操作系统细节,另一方面,使用Java编写的程序将被编译成格式严格同一的字节码存放在.Class文件中,这使得运行在不同平台上的各种不同的Java虚拟机也都能运行相同的Java程序。
事实上Java虚拟机+字节码的组合不仅仅让Java语言运行在不同的平台上,图灵完备的字节码结构还使得任何其他功能性语言都可以被表示为能被Java虚拟机所接受的Class文件。(图灵完备指可以用来模拟单带图灵机的 一系列操作数据的规则)。目前Java虚拟机已经支持Kotlin、JRuby、Scale等语言
Class文件是一组以8个字节为基础单位的二进制流,其中各个数据项目按照顺序紧凑的排列在文件中,并且中间没有空隙存在,当遇到某一项需要使用8个字节以上的空间时,会按照高位在前的方式分割成若干个8个字节进行存储。
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 Number,它的唯一作用适用于标识该文件是一个可以被JVM运行的Class文件,它的16进制值为0xCAFEBABE,这也很容易让人联想到Java的咖啡Logo。
魔数后面紧接着的两个字节(第5、6个字节)是次版本号(Minor Version),再后面两个字节(第7、8个字节)时主版本号。JDK1.1使用的主版本号为45,之后的每个版本一次向上加1。JDK版本向下兼容,但不向上兼容。在Class文件校验过程中即使文件格式为发生任何变化,虚拟机也会拒绝执行超过其版本的Class文件。
我们编写一个简单的HelloWorld的程序
public class Test
{
public static void main(String[] args)
{
hello();
}
public static void hello(){
System.out.println("Hello World!");
}
}
我们可以使用javap -v命令查看编译好后的class文件的版本号,可以看到我使用的是JDK1.8,对应的主版本号是52,而class文件中的第7、8个字节为0x0034,转换为十进制后值为52
关于此版本号为0的情况,在JDK1.2之后,到JDK12之前此版本号均为使用,均固定为0,而在JDK12时,由于JDK提供的功能十分庞大,一些复杂的新特性需要以公测的形式放出,因此重启了次版本号
在主版本号之后存放着常量池的入口。常量池可以比喻为Class文件的资源仓库,它是Class文件中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项之一。
常量池中常量的数量通常是不固定,因此在常量池的入口处需放置一个u2类型的无符号数(第9、10个字节)来记录常量的数量。
以上面的HelloWorld程序为例,其第9,、10字节为0x0020,其十进制值为32,因此该Class文件中有32-1=31个常量
我们依然可以使用javap -v命令查询到class文件中的常量,我们可以看到的确有31个常量
我们可以从上面查询到的信息中看到,常量被分成了多个类型。常量池中的常量大体上分为两个类:字面量与符号引用。字面量通常即字符串文本、常量值等。而符号引用则包括:
这些符号引用需要虚拟机在加载类时进行转换才能获得真正的内存入口地址
常量池中的每一项都以表的形式存在,截至JDK13,常量表中已有以下17个不同类型的常量
类型 | 标志 | 描述 |
---|---|---|
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_MthodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_Dynamic_info | 17 | 动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 一个模块中开放或导出的包 |
下表示每一个常量类型所对应的的表结构
< <常量类型 | 名称 | 数据类型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | |
bytes | u1 | 长度为length的字符串 | |
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 |
bytes | u2 | 指向全限定名常量的索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
bytes | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
index | u2 | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | |
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | |
index | u2 | 指向该字段或方法描述符常量向的索引 | |
CONSTANT_MethodHandle_info | tag | u1 | 值为15 |
reference_kind | u1 | 值在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文件中查找比对出常量池中的所有常量。例如Hello.class文件中紧接在constant_pool_count后面的是0x0A,其十进制值为10,这表示该项是一个方法引用CONSTANT_Methodref_info,后面两个字节分别为0x0007和0x0011,分别指向第7个和第17个常量项,分别表示方法的类标识和名称,而第7个常量项的flag值为0x07,这意味着这是一个CONSTANT_Class_info,,并且后面的两个字节值为0z0019,其值为25 ,指向了第25个常量项,并且该常量项应当是一个UTF-8字符串,代表着该类的全限定名。第17个常量项是一个CONSTANT_NameAndType_info常量。第25个常量项是一个CONSTANT_String_info常量,其字符串长度length为16,length后面的6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74的ASCII码应当对应着java/lang/Object。以此类推,得到的结果应当和上面通过javap -v命令得到的常量表是一致的
在常量池之后紧接着的两个字节是访问标志(access_flag),该标志用于标识类或接口的访问信息,包括:该Class表示类还是接口、是否为public、是否为Abstract类型、是否被声明为final等。具体的访问标志如下:
标志名称 | 标志值 | 二进制位 | 含义 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | 0000000000000001 | 标识是否为public |
ACC_FINAL | 0x0010 | 0000000000010000 | 是否被声明为final,仅类可用 |
ACC_SUPER | 0x0020 | 0000000000100000 | 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2后该标志均为真 |
ACC_INTERFACE | 0x0200 | 0000001000000000 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 0000010000000000 | 是否是一个Abstract类型,抽象类与接口该标志均为真 |
ACC_SYNTHETIC | 0x1000 | 0001000000000000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 0010000000000000 | 标识这是注解 |
ACC_EMUM | 0x4000 | 0100000000000000 | 标识这是枚举 |
ACC_MODULE | 0x8000 | 1000000000000000 | 标识这是模块 |
依然以上面的Hello.class为例,而该类是一个public的普通类,并且使用可JDK1.8,因此该类的ACC_PUBLIC与ACC_SUPER标志均为真,那么该类的访问标志应该为0x0001|0x0020=0x0021。通过查看class文件我们也可以看到访问标志的确为0x0021
访问标志之后的四个字节代表着类索引与父类索引,类索引与父类索引都是u2类型的无符号数;紧随其后的是一组u2类型的数据的集合,其表示接口索引。在Class文件中由这三项来确定该类型的继承关系。类索引用于确定该类的全限定名。父类索引用来确定其父类的全限定名,由于Java不允许多继承,因此父类索引只有一项并且,由于java.lang.Object是所有类的根类,因此除了Object之外所有的类的父类索引均不为0。接口索引用于确定该类实现的接口,由于一个类可以实现多个接口,接口索引由一个接口索引集合来表示。
类索引与父类索引都是u2类型的数据,因此这没有什么可以讨论的。而对于接口索引集合,其在入口处的第一项为u2类型的接口计数器(interfaces_count),用来记录实现的接口数量(与常量池类似)。若该类没有实现任何接口,那么interfaces_count值为0x0000,那么显然后面也就不存在接口的索引表
字段表用于描述接口或类中声明的变量。要注意的是Java中的字段包括类变量和实例变量,但不包括方法内部声明的局部变量。与接口索引集合一致,在字段表的入口处使用了一个u2类型的数据描述了字段表的个数。
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 字段访问标志 |
u2 | name_index | 1 | 对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示字段的简单名称 |
u2 | descriptor_index | 1 | 对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示字段的描述符,用来确定数据类型 |
u2 | attributes_count | 1 | 字段属性数量 |
attribute_info | attributes | attributes_count | 字段属性 |
access_flags与整个Class文件中的访问标志类似,用于指明该字段的访问修饰,具体的标志位如下表。显然ACC_PUBLIC、ACC_PRIVATE与ACC_PROTECTED不可能同时为真。此外接口中的字段必须有ACC_PUBLIC、ACC_STATIC与ACC_FINAL标志
标志名称 | 标志值 | 含义 |
---|---|---|
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_SYSTHETIC | 0x1000 | 表明字段是否是由编译器自动产生 |
ACC_EMUM | 0x4000 | 表明字段是否是emum |
descriptor_index用来描述字段的数据类型,其引用了CONSTANT_Utf8_info中的字符或字符串作为标识字符来确定字段类型,具体的表示字符如下。其中需要注意的是若字段是一个数组,则每一个维度在表示字符前加上一个**‘[’**,例如char[]类型的数组,被记录为"[C",再如String[][]类型的二维数组被记录为"[[Ljava/lang/String"
标识字符 | 含义 |
---|---|
B | 基本数据类型byte |
C | 基本数据类型char |
D | 基本数据类型double |
F | 基本数据类型float |
I | 基本数据类型int |
J | 基本数据类型long |
S | 基本数据类型short |
Z | 基本数据类型boolean |
L | 对象类型,L后面需加上类的全限定名,如Ljava/lang/String |
方法表用于描述类或接口的方法信息,它的大致格式与字段表集合如出一辙
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | access_flags | 1 | 方法访问标志 |
u2 | name_index | 1 | 对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示方法的简单名称 |
u2 | descriptor_index | 1 | 对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示方法的描述符,用来确定返回数据类型 |
u2 | attributes_count | 1 | 方法属性数量 |
attribute_info | attributes | attributes_count | 方法属性 |
方法的访问标志access_flags与字段的差别在于volatile与transient不能修饰方法,因此访问标志中没有ACC_VOLATILE和ACC_TRANSIENT,此外,synchronized、native、strictfp和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_STRICT | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器产生 |
descriptot_index项与字段表基本一致,唯一的差别是方法允许空返回值,因此增加了一个表示字符V表示void
对于方法来说,最重要的是方法中的代码,而这部分代码编译形成的字节码将会存储在方法表属性集合的Code属性中
前面在字段表与方法表中已经见到了有属性表,事实上,Class文件、字段和方法都可以携带自己的属性表,因此上面字段表部分没有做详解。与其余的数据项目不同的是,属性表并没有对表中的属性有严格的顺序要求,并且只要不与已有属性重名,任何人都可以在实现编译器时向属性表中写入自定义的属性,但Java虚拟机在运行时会忽略掉不认识的属性。具体的属性项如下表。由于属性太多太杂,所以暂时不展开来整理了,后面有时间单独开一篇整理一下。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字修饰的常量 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exception | 方法表 | 方法抛出的异常列表 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或匿名类时才能拥有这个属性,用于标示这个类所在的外围方法 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
StackMapTable | Code属性 | Jdk1.6中增加的属性,供类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature | 类、方法表、字段表 | jdk1.5中增加的属性,用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | jdk1.5中增加的属性,用于存储额外的调试信息 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成 |
LocalVariableTypeTable | 类 | jdk1.5中增加的属性,使用特征签名代替描述符 |
RuntimeVisibleAnnotations | 类、方法表、字段表 | jdk1.5中增加的属性,为动态注解提供支持,用于指明哪些注解式运行时可见的 |
RuntimeInvisibleAnnotations | 类 、方法表、字段表 | jdk1.5中增加的属性,指明哪些注解式运行时不可见的 |
RuntimeVisibleParameterAnnotations | 方法表 | jdk1.5中增加的属性,与RuntimeVisibleAnnotations类似,但作用对象为方法参数 |
RuntimeInvisibleParameterAnnotations | 方法表 | jdk1.5中增加的属性,与RuntimeInvisibleAnnotations类似,但作用对象为方法参数 |
AnnotationsDefault | 方法表 | jdk1.5中增加的属性,用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | jdk1.7中增加的属性,用于保存invokedynamic指令引用的引导方法限定符 |
RuntimeVisibleTypeAnnotations | 类、方法表、字段表、Code属性 | jdl1.8中增加的属性,为JSR308中新增的类型注解提供支持,指明哪些类注解是运行时可见的 |
RuntimeInvisibleTypeAnnotations | 类、方法表、字段表、Code属性 | jdl1.8中增加的属性,为JSR308中新增的类型注解提供支持,指明哪些类注解是运行时不可见的 |
MethodParameters | 方法表 | jdk1.8中增加的属性,用于支持将方法名称编译进class文件中,并运行时可获取 |
Module | 类 | jdk9中增加的属性,用于记录Module的名称及相关信息 |
ModulePackages | 类 | jdk9中增加的属,用于记录一个模块中所有被exports或opens的包 |
ModuleMainClass | 类 | jdk9中增加的属性,用于指定一个模块的主类 |
NestHost | 类 | jdk11中增加的属性,,用于支持嵌套类的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类 |
NestMembers | 类 | jdk11中增加的属性,用于支持嵌套类的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类 |