zl程序教程

您现在的位置是:首页 >  后端

当前栏目

JVM学习笔记(二)——Class文件结构

JVM文件笔记学习 结构 Class
2023-09-11 14:18:21 时间

Class文件是Java程序跨平台的保证,正是由于有了Class文件架起源码和机器码之间的中间桥梁,JVM虚拟机才可以在各种平台上按照统一的规范标准加载Java代码。

作为“写给虚拟机看的”Java代码,Class文件结构必须设计得足够完善,同时由于Java虚拟机规范并不只针对Java,Class文件又不能引入过多细节。本篇博客我们就来介绍下Class文件的结构。

一个Class文件对应一个Java Class,所以一个Class文件记录着一个类的全部信息,JVM通过Class文件将对应的类加载入内存。

Class文件的结构主要分为以下几部分:

类索引、父类索引、接口索引 字段表集合 方法表集合 索引表集合

每个Class文件的头4个字节成为魔数(Magic Number),它的唯一作用就是确定这个文件是否能作为一个Class文件被接受。很多文件都以魔数进行类型识别,如gif、jpeg等图片文件。之所以使用魔数而不是扩展名是处于安全考虑,文件扩展名可以所以改动。Class文件的魔数是0xCAFEBABE。

紧接着魔数的4个字节存储的是Class文件的版本号,5、6字节为次版本号,7、8字节为主版本号。不同版本的虚拟机可以接受不同版本的class文件,所以虚拟机通过主次版本号判断是否可以加载目标class文件。

2 常量池

常量池可以看做是Class文件的资源仓库,也是Class文件中占用空间最大的部分。常量池主要存放两大类常量:字面量、符号引用。

字面量比较接近Java语言层面的常量,如文本字符串、生命为final的常量等。

符号引用属于编译范畴中的概念,主要包括三类常量:

类和接口的全限定名 字段的名称和描述符 方法的名称和描述符

Java语言不同于C、C++等语言在编译阶段即进行链接,相应的链接都放到了运行时阶段。所以Class文件中不可能包含各个方法、字段在内存中的布局。Java虚拟机在运行阶段加载类时,将符号引用转换成真正的内存入口地址,对应类才算可以工作。

常量池中的每一项代表一个常量,JDK目前共有14中类型的常量,而每一个常量又有自己的内部结构。类或接口符号索引是其中较为简单的一项,接下来以类索引为例做简单介绍。类符号索引对应的类型为CONSTANT_Class_info,其结构如下:


tag是标志位,表明类型。CONSTANT_Class_info的tag为7。name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类的全限定名。

CONSTANT_Utf8_info的结构如下所示:


3 访问标志

常量池之后的两个字节代表访问标志(accss_flags),用于识别类或接口的层次访问信息:


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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据集合。Class文件中的这三项决定了类的继承关系。

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info类描述符常量,通过CONSTANT_Class_info类型的常量索引值可以找到定义在CONSTAN_Utf8_info类型的常量中的类全限定名。

对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count)表示索引表的容量。每个接口的同样由一个u2类型数据指向一个CONSTANT_Class_info。

5 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量和实例级变量,但不包括定义在方法内部的局部变量。每个字段的结构如下图所示:


5.2 name_index

name_index标识字段的简单名称。简单名称和全限定名的区别在于:全限定名是类的全路径名,如org/fenixsoft/clazz/TestClass,只是把类全名中的"."替换成“/”而已。简单名称指的是没有类型和参数修饰的方法或者字段名称,如一个类中含有一个字段"m",则其简单名称为"m"。

5.3 descriptor_index

descriptor_index为字段或方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型以及代表无返回值的void以及对象类型均由一个大写字符来代替:


对于数组类型,每一个维度用一个"["来描述,比如定义一个“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/string”。

方法描述符按照先参数列表后返回值的顺序描述,参数列表按照参数顺序放在一组"()"之内。如方法int indexOf(char[] source, int sourceOffest, int sourceCount, char[] target, int targetOffest, int targetCount, int fromIndex)的描述符为"([CII[CIII)I"。

5.4 attributes_count attribute_info

在描述符之后还有数量为attributes_count的attribute_info,attribute_info描述字段的额外信息,但这些额外信息最终存放在属性表中。如“final static int m = 123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。

6 方法表集合

方法表和字段表结合几乎一样,理解了字段表,方法表就非常简单了。


由于volatile和transient不能修饰方法,所以方法表的访问标识中没有了ACC_VOLATILE,ACC_TRANSIENT标识。但同时又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。

需要说明的是,方法表集合中并不包含方法里面的代码。方法代码经过编译后存放在方法属性集合中的一个名为"Code"的属性里面。例如某方法的属性表计数器attributes_count为1,则表示方法的属性表集合有一项属性,属性索引名称为0x0009,对应常量为code,说明此属性是方法的字节码描述。

7 属性表集合

Class文件、字段表、方法表都可以有自己的属性表,Java7里面定义了21种属性。

Code属性

并非所有方法表都有Code属性,比如接口和抽象类的方法就没有。结构如下:


局部变量表所需存储空间,单位是Slot,double和long占用2个Slot、其他基本类型1Slot,Slot空间可以重用(变量作用域问题)
编译后的字节码长度,理论上最长2^32-1,实际上JVM规定一个方法不允许超过65535条字节码指令
exception_table_length 异常表,记录字节码在start_pc到end_pc行之间如果出现类型为catch_type或其子类的异常则跳转到handler_pc行继续处理

字节码值得注意的一个地方是,javac编译时将this关键字作为一个普通方法参数由JVM调用时自动传入。

Exceptions属性

描述方法可能抛出的受检异常。

LineNumberTable属性

描述Java远吗行号与字节码行号之间映射关系,也就是为什么抛异常的时候可以显示源码哪一行抛出的。

LocalVariableTable属性

描述栈桢中局部变量表与Java源码中变量的关系,以保证编译后的代码被其他代码调用时,IDE可以显示参数名(否则被arg0、arg1之类的变量名代替)

SourceFile属性

描述生成当前Class文件的源文件名称,也是抛异常时可以显示源文件名字的原因。但内部类不会生成这个属性。

ConstantValue属性

static关键字修饰的变量可以使用这个属性。对于Sun javac编译器,final static的变量采用ConstantValue属性初始化,其他static变量在(类构造器)中初始化。

InnerClasses属性

记录内部类和宿主类的关联。内部类和宿主类的Class文件都会有这个属性。

Signature属性

记录泛型签名信息。Java的泛型是使用擦除式实现的伪泛型,编译后擦除泛型,这个属性为了弥补此缺陷,方便反射API可以拿到泛型类型。