Java Class类文件结构

Java Class类文件结构

文章目录

    • Java Class类文件结构
      • 一、字节码——无关性的基石
      • 二、纵观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文件的由以下的数据项按照顺序严格排列。当某一数据类型有多个数据但数量不定时,常常使用一个前置的无符号数作为容量计数器

类型 名称 数量
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
Java Class类文件结构_第1张图片
在这里插入图片描述

关于此版本号为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个常量
Java Class类文件结构_第2张图片
我们可以从上面查询到的信息中看到,常量被分成了多个类型。常量池中的常量大体上分为两个类:字面量与符号引用。字面量通常即字符串文本、常量值等。而符号引用则包括:

  • 被模块导出或开放的包
  • 类和接口全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
  • 方法句柄和方法类型
  • 动态调用点和动态常量

这些符号引用需要虚拟机在加载类时进行转换才能获得真正的内存入口地址

常量池中的每一项都以表的形式存在,截至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命令得到的常量表是一致的

Java Class类文件结构_第3张图片

五、访问标志

在常量池之后紧接着的两个字节是访问标志(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

Java Class类文件结构_第4张图片

六、类索引、父类索引与接口索引集合

访问标志之后的四个字节代表着类索引与父类索引,类索引与父类索引都是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,一个宿主类通过该属性得知自己有哪些内部类

你可能感兴趣的:(Java,java,jvm)