深入理解JAVA虚拟机(三):类文件的结构

本文详细介绍了Java Class文件的结构组成,包括魔数与版本号、常量池、访问标志等关键部分,深入剖析了Class文件如何存储类信息。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包括了Java虚拟机指令集和符号表以及若干其他辅助信息。例如,使用Java编译器可以把JAva代码编译为存储字节码的Class文件,使用JRuby等其他语言的编译器一样也可以把程序代码编译成Class文件,虚拟机不关心Class来源于何种语言。

注意:任何一个Class文件都对应着唯一一个类或接口的定义信息,但类或接口并不一定都得定义在文件里,比如类或接口也可以通过类加载器直接生成。

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,则会根据高位在前的方式分割成若干个8位字节进行存储。Class文件格式中有两种数据类型:无符号数和表。

无符号数:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表:表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表。

如下图是Class文件的构成的数据项:


1、魔数与Class文件的版本

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class文件的魔数值是0xCAFEBABE。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。

2、常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型。常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。以以下java代码为例,编译后的class文件如图所示:

public class TestClass {
	private int m;
	public int inc(){
		return m + 1;
	}
}


如图所示为0x0016即十进制的22,这代表常量池有21项常量,索引范围是1-21。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比如文本字符串、声明为final的常量值等,而符号引用则包括三类常量:A、类和接口的全限定名(Fully Qualified Name) B、字段的名称和描述符(Descriptor)C、方法的名称和描述符。当虚拟机运行时,需要从常量池获得对应的符合引用,再在类创建时或运行时解析、翻译到具体的内存地址中。常量池中每一项常量都是一个表,这14张表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag,如下表2所示),代表当前这个常量属于哪种常量类型。


而这14中常量类型各自均有自己的结构。在图1中常量池的第一项常量,它的标志位是0x07,查看表2的标志列可知这个常量属于CONSTANT_CLASS_info类型,其结构如表3所示:

name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类(或者接口)的全限定名,这里name_index值为0x0002,即是指向了常量池中的第二项常量。查看图1可知第二项常量的标志位是0x01,确实是CONSTANT_Utf8_info类型常量,CONSTANT_Utf8_info类型的结构如表4所示:


length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。

注意:Class文件中的方法、字段等都需要引用CONSTANT_Utf8_info类型常量来描述名称,所以CONSTANT_Utf8_info类型常量的最大长度就是Java中方法,字段名的最大长度。这里最大长度是u2类型的值就是65535,所以程序中超过64KB英文字符的变量或方法名,无法编译。

Oracle公司有专门用于分析Class文件字节码的工具:javap,在命令行敲入javap -verbose TestClass可以解析出TestClass.class文件字节码内容。

如下图是常量池中的14中常量项的结构表:


3、访问标志

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


以本文中的代码为例,TestClass是一个普通的Java类被public修饰,因此它的标志应当为0x0001|0x0020=0x0021

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

类索引:一个u2类型的数据,用于确定这个类的全限定名,指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

父类索引:一个u2类型的数据,用于确定这个类的父类的全限定名,指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

接口索引:一组u2类型的数据的集合,描述这个类实现了哪些接口。

5、字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。如下表8列出了字段表的格式:


字段修饰符放在access_flags项目中,它与类中的access_flags项目非常类似,都是一个u2的数据类型,其中可以设置的标志位和含义见表9


跟随access_flags标志的是两项索引值:name_index和descriptor_index,都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。方法和字段的描述符是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。描述符的字含义见表10

对于数组类型,每一维度将使用一个前置的“[”字符来描述,如“java.lang.String[][]”将被记录为“[[Ljava/lang/String;”,"int[]"被记录为“[I”。
用描述符来描述方法时,按照先参数列表,后返回值的顺序,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法int indexOf(char[] s,int a,int b)的描述符为“([CII)I”
如果字段表中有需要额外描述的信息,会在固定数据项之后跟着一个属性表集合,比如将字段m的声明改为“final static int m = 123;”那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。
字段表集合中不会列出从超类或者父接口继承的字段。

6、方法表集合

Class文件存储格式对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样。而方法中的代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。方法表中有可能会出现编译器自动添加的方法,最典型的的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。

7、属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合。属性表集合不要求各个属性表具有严格的顺序,只要不与已有属性名重复即可。下表是虚拟机规范预定义的属性:


每个属性都需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构是自定义的,只需要一个u4的长度说明属性值所占的位数即可。

(1)Code属性表

Code属性表结构如下表,接口和抽象类中的方法不存在Code属性表。


max_stack代表了操作数深度的最大值。在方法运行的任何时刻,操作数站不会超过这个深度。虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。单位是Slot,是虚拟机为局部变量分配内存所用的最小单位。而且Slot是可以重用的。code用于存储源程序编译后生成的字节码指令,每个指令是u1类型的单字节,当虚拟机读取到一个字是可以对应找出这个字节码代表的指令。code_length是一个u4类型的长度值,理论上可以达到2的32次方减1,但是虚拟机限制了一个方法中不允许超过65535条字节码指令。

(2)Exceptions属性

此属性可以列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常,Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常是一个exception_index_table,而exception_index_table指向常量池中CONSTANT_Class_info型常量的索引。

(3)LineNumberTable属性

此属性用于描述Java源码行号与字节码行号的关系,可以在程序运行抛出异常时显示出错的行号。line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info还包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是java源码行号。

(4)ConstantValue属性

作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类变量)才可以使用这项属性。对于非static类型的变量的赋值是在实例构造器<init>方法中进行的,而对于类变量,如果同时使用final和static来修饰,并且这个变量的数据类型是基本类型或者是java.lang.String类型的,就生成ConstantValue属性来进行初始化,如果只被static修饰,或者并非基本类型及字符串,则会在<clinit>方法中初始化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值