【JVM进阶之路】十一:Class文件结构

Java虚拟机和Class文件是Java实现系统无关性的基石。

Class文件是JVM实现语言无关性的基石。

【JVM进阶之路】十一:Class文件结构_第1张图片

Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。

每一个 Class 文件对应于一个如下所示的 ClassFile 结构体:

ClassFile {
     
 u4 magic;
 u2 minor_version;
 u2 major_version;
 u2 constant_pool_count;
 cp_info constant_pool[constant_pool_count-1];
 u2 access_flags;
 u2 this_class;
 u2 super_class;
 u2 interfaces_count;
 u2 interfaces[interfaces_count];
 u2 fields_count;
 field_info fields[fields_count];
 u2 methods_count;
 method_info methods[methods_count];
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

简单看一下各项的含义:

【JVM进阶之路】十一:Class文件结构_第2张图片

由于 Class 文件结构没有任何分隔符,所以无论是每个数据项的的顺序还是数量,都是严格限定的,哪个字节代表什么含义,长度多少,先后顺序如何,都是不允许改变的。

接下来我们来具体学习每项的含义。

1、魔数

这是基本上每个Java开发人员的第一个Java程序:

public class HelloWorld {
     
    public static void main(String[] args) {
     

        System.out.println("Hello World");
    }
}

我使用的是Idea工具,运行,target目录下会生成对应的class文件,为了查看文件的十六进制信息,我们可以安装一个插件HexView

【JVM进阶之路】十一:Class文件结构_第3张图片

安装完成之后,选择class文件,右键 HexView,打开后的十六进制如下:

【JVM进阶之路】十一:Class文件结构_第4张图片

第一行中有一串特殊的字符 cafebabe,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 ClassFormatError

这段字节很有意思——咖啡宝贝,Java原来不止是咖啡,还是宝贝

2、版本号

紧跟着魔数后面的四个字节 0000 0031 存储的是 class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。

Java的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加1(JDK1.0JDK1.1使用了45.045.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。

0000 0031 对应的十进制为49,是JDK8的内部版本号。

3、常量池

紧接着主、次版本号之后的是常量池入口。

由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的。

【JVM进阶之路】十一:Class文件结构_第5张图片

如图所示,常量池容量为十六进制数0x0022,即十进制的34,这就代表常量池中有33项常量,索引值范围为1~33。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。

常量池中主要存放两大类常量:字面量(Literal)符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 被模块导出或者开放的包(Package)

  • 类和接口的全限定名(Fully Qualified Name)

  • 字段的名称和描述符(Descriptor)

  • 方法的名称和描述符

  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)

  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

这17类常量结构只有一个相同之处,表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型。

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_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Moudle_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或者导出的包

常量池非常繁琐,17种常量类型各自有着完全独立的数据结构,彼此之间没有什么共性和联系。

我们直接看一下常量池中的17种数据类型的结构总表:

【JVM进阶之路】十一:Class文件结构_第6张图片

【JVM进阶之路】十一:Class文件结构_第7张图片

【JVM进阶之路】十一:Class文件结构_第8张图片

4、访问标志

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等。

具体的标志位以及标志的含义如表:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为 Public 类型
ACC_FINAL 0x0010 是否被声明为 final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令的新语义
ACC_INTERFACE 0x0200 标志这是一个接口
ACC_ABSTRACT 0x0400 是否为 abstract 类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标志这是一个注解
ACC_ENUM 0x4000 标志这是一个枚举

access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一 律为零。

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

这三者为什么放在一起呢?因为这三者用来确定类的继承关系。

类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(后的接口顺序从左到右排列在接口索引集合中。

6、字段表集合

接口索引结束后,接着是字段表(field_info),它用于描述接口或者类中声明的变量——这里的字段(Field)只包括类级变量以及实例级变量,不包括在方法内部声明的局部变量。

描述的主要信息包括:

①、字段的作用域(public,protected,private修饰)

②、是类级变量还是实例级变量(static修饰)

③、是否可变(final修饰)

④、并发可见性(volatile修饰,是否强制从主从读写)

⑤、是否可序列化(transient修饰)

⑥、字段数据类型(8种基本数据类型,对象,数组等引用类型)

⑦、字段名称

字段表的结构如下:

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

access_flags是该字段的的访问标志,它和类中的访问标志很类似,用以描述该字段的权限类型:private、protected、public;并发可见性:volatile;可变性:final;

访问标志详情如下图所示:

【JVM进阶之路】十一:Class文件结构_第9张图片

由于Java语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

7、方法表集合

方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如表所示:

【JVM进阶之路】十一:Class文件结构_第10张图片

有区别的部分只有方法访问标志access_flag, 因为volatile关键字和transient关键字不能修饰方法。

方法表标志位及其取值如下:

【JVM进阶之路】十一:Class文件结构_第11张图片

8、属性表集合

接下来终于到了最后一项:属性表集合。

前面提到的Class文件、字段表、方法表都可以携带自己的属性表集合,就是引用的这里。

属性表集合中的属性如下所示:

【JVM进阶之路】十一:Class文件结构_第12张图片

【JVM进阶之路】十一:Class文件结构_第13张图片

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。




参考:

【1】:《深入理解Java虚拟机:JVM高级特性与最佳实践》第三版

【2】:《Java虚拟机规范(Java_SE_7)》

【3】:jvms/se8

【4】:Java虚拟机详解(九)------类文件结构

【5】:一把小刀,直插 class 文件的小心脏

【6】:JVM系列(二)-字节码文件结构(基础篇)

你可能感兴趣的:(JVM,jvm,JVM进阶之路)