Java Class类文件结构

Java Class类文件结构

一、字节码——无关性的基石

当我们使用命令行来运行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文件结构

Class文件是一组以8个字节为基础单位的二进制流,其中各个数据项目按照顺序紧凑的排列在文件中,并且中间没有空隙存在,当遇到某一项需要使用8个字节以上的空间时,会按照高位在前的方式分割成若干个8个字节进行存储。

Class文件使用一种类似于C语言结构体的伪结构来存储数据,其中包含两种数据类型——无符号数与表。

无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节与8个字节的无符号数。无符号数通常可以用来描述数字、引用索引、数量值或按照UTF-8编码构成的字符串值

表是由多个无符号数或其他表作为数据项组成的复合数据结构。表的命名通常以"_info"结尾。表用于描述有层次关系的复合结构的数据。整个Class文件也可以视为一张表

整个Class文件的由以下的数据项按照顺序严格排列。当某一数据类型有多个数据但数量不定时,常常使用一个前置的无符号数作为容量计数器

类型名称数量
u4magic1
u2minor_version1
u2major_version1
u2constant_pool_count1
cp_infoconstant_poolconstant_pool_count -1
u2access_flags1
u2this_class1
u2super_class1
u2interfaces_count1
u2interfacesinterfaces_count
u2fields_count1
field_infofieldsfields_count
u2methods_count1
method_infomethodsmethods_count
u2attributes_count1
attribute_infoattributesattributes_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_info1UTF-8编码的字符
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MthodHandle_info15方法句柄
CONSTANT_MethodType_info16方法类型
CONSTANT_Dynamic_info17动态计算常量
CONSTANT_InvokeDynamic_info18动态方法调用点
CONSTANT_Module_info19表示一个模块
CONSTANT_Package_info20一个模块中开放或导出的包

下表示每一个常量类型所对应的的表结构

< <
常量类型名称数据类型描述
CONSTANT_Utf8_infotagu1值为1
lengthu2UTF-8编码的字符串占用的字节数
bytesu1长度为length的字符串
CONSTANT_Integer_infotagu1值为3
bytesu4高位在前存储的int值
CONSTANT_Float_infotagu1值为4
bytesu4高位在前存储的Float值
CONSTANT_Long_infotagu1值为5
bytesu8高位在前存储的long值
CONSTANT_Double_infotagu1值为6
bytesu8高位在前存储的double值
CONSTANT_Class_infotagu1值为7
bytesu2指向全限定名常量的索引
CONSTANT_String_infotagu1值为8
bytesu2指向字符串字面量的索引
CONSTANT_Fieldref_infotagu1值为9
indexu2指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
indexu2指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_infotagu1值为10
indexu2指向声明方法的类描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_InterfaceMethodref_infotagu1值为11
indexu2指向声明方法的类描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_infotagu1值为12
indexu2指向该字段或方法名称常量项的索引
indexu2指向该字段或方法描述符常量向的索引
CONSTANT_MethodHandle_infotagu1值为15
reference_kindu1值在1至9之间,决定了方法句柄的类型,表名方法句柄的字节码行为
reference_indexu2值时对常量池的有效索引
CONSTANT_MethodType_infotagu1值为16
descriptor_indexu2对常量池的有效索引,并且必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_Dynamic_infotagu1值为17
bootstrap_method_attr_indexu2对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_indexu2对当前常量池的有效索引,并且必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_InvokeDynamic_infotagu1值为18
bootstrap_method_attr_indexu2对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_indexu2对当前常量池的有效索引,并且必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符
CONSTANT_Module_infotagu1值为19
name_indexu2对常量池的有效索引,并且必须是CONSTANT_Utf8_info结构,表示模块名称
CONSTANT_Package_infotagu1值为20
name_indexu2对常量池的有效索引,并且必须是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_PUBLIC0x00010000000000000001标识是否为public
ACC_FINAL0x00100000000000010000是否被声明为final,仅类可用
ACC_SUPER0x00200000000000100000是否允许使用invokespecial字节码指令的新语义,JDK1.0.2后该标志均为真
ACC_INTERFACE0x02000000001000000000标识这是一个接口
ACC_ABSTRACT0x04000000010000000000是否是一个Abstract类型,抽象类与接口该标志均为真
ACC_SYNTHETIC0x10000001000000000000标识这个类并非由用户代码产生
ACC_ANNOTATION0x20000010000000000000标识这是注解
ACC_EMUM0x40000100000000000000标识这是枚举
ACC_MODULE0x80001000000000000000标识这是模块

依然以上面的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类型的数据描述了字段表的个数。

类型名称数量说明
u2access_flags1字段访问标志
u2name_index1对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示字段的简单名称
u2descriptor_index1对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示字段的描述符,用来确定数据类型
u2attributes_count1字段属性数量
attribute_infoattributesattributes_count字段属性

access_flags与整个Class文件中的访问标志类似,用于指明该字段的访问修饰,具体的标志位如下表。显然ACC_PUBLIC、ACC_PRIVATE与ACC_PROTECTED不可能同时为真。此外接口中的字段必须有ACC_PUBLIC、ACC_STATIC与ACC_FINAL标志

标志名称标志值含义
ACC_PUBLIC0x0001表明字段是否是public
ACC_PRIVATE0x0002表明字段是否是private
ACC_PROTECTED0x0004表明字段是否是protected
ACC_STATIC0x0008表明字段是否是static
ACC_FINAL0x0010表明字段是否是final
ACC_VOLATILE0x0040表明字段是否是volatile
ACC_TRANSIENT0x0080表明字段是否是transient
ACC_SYSTHETIC0x1000表明字段是否是由编译器自动产生
ACC_EMUM0x4000表明字段是否是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

八、方法表集合

方法表用于描述类或接口的方法信息,它的大致格式与字段表集合如出一辙

类型名称数量说明
u2access_flags1方法访问标志
u2name_index1对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示方法的简单名称
u2descriptor_index1对常量池的引用,并且该常量应当是一个CONSTANT_Utf8_info结构的常量,表示方法的描述符,用来确定返回数据类型
u2attributes_count1方法属性数量
attribute_infoattributesattributes_count方法属性

方法的访问标志access_flags与字段的差别在于volatile与transient不能修饰方法,因此访问标志中没有ACC_VOLATILE和ACC_TRANSIENT,此外,synchronized、native、strictfp和abstract可以修饰方法,访问标志中也相应的加上了这四项

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICT0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否由编译器产生

descriptot_index项与字段表基本一致,唯一的差别是方法允许空返回值,因此增加了一个表示字符V表示void

对于方法来说,最重要的是方法中的代码,而这部分代码编译形成的字节码将会存储在方法表属性集合的Code属性中

九、属性表集合

前面在字段表与方法表中已经见到了有属性表,事实上,Class文件、字段和方法都可以携带自己的属性表,因此上面字段表部分没有做详解。与其余的数据项目不同的是,属性表并没有对表中的属性有严格的顺序要求,并且只要不与已有属性重名,任何人都可以在实现编译器时向属性表中写入自定义的属性,但Java虚拟机在运行时会忽略掉不认识的属性。具体的属性项如下表。由于属性太多太杂,所以暂时不展开来整理了,后面有时间单独开一篇整理一下。

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字修饰的常量
Deprecated类、方法表、字段表被声明为deprecated的方法和字段
Exception方法表方法抛出的异常列表
EnclosingMethod类文件仅当一个类为局部类或匿名类时才能拥有这个属性,用于标示这个类所在的外围方法
InnerClasses类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性Jdk1.6中增加的属性,供类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类、方法表、字段表jdk1.5中增加的属性,用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件jdk1.5中增加的属性,用于存储额外的调试信息
Synthetic类、方法表、字段表标识方法或字段为编译器自动生成
LocalVariableTypeTablejdk1.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文件中,并运行时可获取
Modulejdk9中增加的属性,用于记录Module的名称及相关信息
ModulePackagesjdk9中增加的属,用于记录一个模块中所有被exports或opens的包
ModuleMainClassjdk9中增加的属性,用于指定一个模块的主类
NestHostjdk11中增加的属性,,用于支持嵌套类的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类
NestMembersjdk11中增加的属性,用于支持嵌套类的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值