JVM学习笔记: Class文件结构

211 阅读7分钟

Class文件结构

1. 来源

  • 深入理解Java虚拟机:JVM高级特性与最佳实践(第三版)
  • 第六章

2. 概述

本文主要是对第六章内容的知识梳理,介绍了Class文件的结构。

3. Class文件结构概览

image-20220211165218256.png

下面将会逐步分析上图中的各个字段。

4. 详细分析

4.1 魔数

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。魔数固定为0xCAFEBABE,否则会抛出如下错误:

image-20220211165946767.png

4.2 版本号

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

Java的版本号是从45开始的,JDK 1.1之后 的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能 向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文 件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class 文件。

次版本号在Java 2出现之前被短暂使用过,从JDK 1.2之后到JDK 12之前的此版本号均未被使用, 固定为0, JDK 12以后, 一些复杂的功能特性以公测的方式放出, 如果Class文件中使用了该版本JDK的尚未被列入正式特性清单的功能功能, 则必须把次版本号标识为65535

4.3 常量池

常量池入口紧跟在主, 次版本号之后. 常量池可以视为Class文件的资源仓库,它是Class文件与其他项目关联最多的数据。

常量池的数据数量是不定的,所以在常量池的入口有一个u2类型的数据,代表了常量池的容量计数值。这个容量计数不是从0开始的,而是从1开始。这样做的目的在于后面某些指向常量池的索引值在特定情况下需要表达”不引用任何一个常量池项目的含义”,这样就可以把索引值设为0 。除常量池之外,其他集合类型的索引值都是从0开始的。

常量池主要存放两大类常量:

  • 字面量(Literal):如文本字符串、被声明为final的常量值等。

  • 符号引用(Symbolic References):

    • 被模块导出或者开放的包(Package
    • 类和接口的全限定名(Fully Qualified Name
    • 字段的名称和描述符(Descriptor
    • 方法的名称和描述符
    • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic
    • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant

    常量池中每一项常量都是一个表,共有17种不同类型,这些类型的共同点为:在表的起始位置的第一位是一个u1类型的标志位(tag),代表着当前的常量是什么类型。具体的常量池项目类型如图:

image-20220211175525987.png

每个常量类型的具体结构和含义如下:

image-20220211175749262.png

image-20220211175906936.png

image-20220211175926512.png

image-20220211175946570.png

4.4 访问标志

在常量池之后,紧跟者u2类型的访问标志,这个标志用于识别一些类或 者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等。具体的标志位以及标志的含义如下:

image-20220211180208666.png

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

类索引(this_class)与父类索引(super_class)都是一个u2类型的数据;接口索引集合(interface)是一组u2类型的数据的集合。

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类 型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的 全限定名字符串。

image-20220211180623642.png

于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count),表示索引表 的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

*注:上图对应的部分常量池内容:

image-20220211180747731.png

image-20220211180754186.png

4.6 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。字段可以包括的修饰符有字段的作用域(public、private、protected修饰 符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否 强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。

字段表结构:

image-20220211181226677.png

access_flags标志值及其含义:

image-20220211181247861.png

全限定名、简单名称与描述符:

全限定名:“org/fenixsoft/clazz/TestClass”是这 个类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混 淆,在使用时最后一般会加入一个“”号表示全限定名结束。

简单名称:简单名称则就是指没有类型和参数修饰 的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。

描述符:描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类 型(bytechardoublefloatintlongshortboolean)以及代表无返回值的void类型都用一个大 写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见表6-10。

image-20220211181809640.png

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型 的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。

用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序 放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符 为“()Ljava/lang/String;”,方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target, int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”。

4.7 方法表集合

方法表集合与4.6的字段表集合相似,这里就不过多介绍了,就记录一下方法表的结构以及方法访问标志的标志值以及含义:

image-20220211182222636.png

image-20220211182233244.png

4.8 属性表集合

Class文件、字段表、方法表都可以携带自己的属性表集合用来描述某些场景专有的信息。

相比于Class文件,属性表集合的限制稍微宽松,对属性表不要求严格的顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。

虚拟机规范预定义的属性:

image-20220211182732228.png

image-20220211182827941.png

对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。 一个符合规则的属性表应该满足表6-14中所定义的结构。

image-20220211182934936.png

由于预定义属性表中的各项属性的含义与结构很复杂又庞大,笔者后续会另外整理单独的笔记来介绍这不分内容。