深入理解Java虚拟机读书笔记(8): 深入理解类文件结构

深入理解Java虚拟机读书笔记(8): 深入理解类文件结构

随着语言的发展,Java虚拟机已经不再仅仅是针对“Java”这一门语言而言了,语言无关性被越来越重视。虚拟机平台使用统一的程序存储格式字节码(ByteCode),实现平台无关性。然而,一大批在Java虚拟机上运行的语言如Clojure、Scala等发展越来越快。但是,实现语言无关性的基础仍然是虚拟机和字节码存储格式。 Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。

Class文件结构

1.1 基本概念

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

Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,其中只有两种数据类型:无符号数

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

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

1.2 魔数

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

以一段简单的代码进行编译为例,java代码如下:

package com.wds.clazz;

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

使用WinHex软件打开Class文件,结果如下:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

可以清晰的看见开头4个字节的16进制表示是0xCAFEBABE,代表次版本号的第5和第6字节值为0x0000,主版本号的值为0x0034,换成十进制为52。可知这个文件是可以被jdk1.8及以上虚拟机执行的class文件。

1.3 常量池

主版本号后面则是常量池,常量池中常量数量是不固定的,所以需要在其入口处放置一项u2类型的数据,代表常量池容量计数值。注意,这个值是从1开始的。从上面的图中可以发现,常量池容量(偏移地址0x00000008)为十六进制数0x0013,转换为十进制为19,代表常量池中有19个常量,索引值为1~18。这点可以通过jdk自带的工具javap来看查看,其参数-verbose输出class文件字节码内容。如下所示:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

将第0项常量空出来的原因:满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池 项目”的含义,这种情况就可以把索引值置为0来表示。

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)

字面量比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。

符号引用则属于编译原理方面的概念,包括了下面三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。 也就是说,在Class文件中不会保存各个方法、 字段的最终内存布局信息,因此这些字段、 方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、 翻译到具体的内存地址之中

常量池中每一项常量都是一个表,在JDK1.7中一共有14种,查阅文档发现jdk1.8中没有变化,jdk1.8文档地址

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

14种常量类型数据结构均不相同,非常繁琐,如下图所示:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

  • 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

  • 这14种常量项结构还有一个特点是,其中13表占用得字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,无法确定大小不固定,编译后,通过utf-8编码,就可以知道其长度。

    另外,由于Class文件中方法、 字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、 字段名的最大长度。 而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535(u2代表2个字节,16bit,2^16=65536)。 所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

查找TestClass.class文件的常量池内容

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

由上图可知常量池中第一项常量标志的16进制值是0x0A=10,查表发现这个常量属于CONSTANT_Methodref_info类型,此类型表示类中方法的符号引用。查看该类型的结构如下:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

CONSTANT_Methodref_info型常量的第二个数据项为index,类型是u2,index存储的是一个索引值,从class文件中查得该值为0x0004=4,即它指向常量池中第4个常量;第三个数据项也是索引其值为0X000F=15,指向常量池种第15个常量

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

CONSTANT_Methodref_info表示的是类中方法的符号引用,所以index中存放的不是一个具体得内容,而是一个索引位置,所以说其具体内容存放在另一个常量项中。找第4个常量项之前需要知道第4个常量项的开始位置,所以需要知道前3个常量项所占字节数。那就看第2个常量项,由于第一个常量项共占了5个字节,则紧接着的字节就为第二个常量项的tag,如下图可得其值为0X09=9,说明第2个常量项得项目类型为CONSTANT_Fieldref_info。查表得其该类型得字节长度固定占5个字节。如下图所示:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

依次类推查的第3,4个常量项为CONSTANT_Class_info型。如下图:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

CONSTANT_Class_info存放的是指向类或接口的符号引用,其内容为0x070011和0x070012。

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

根据CONSTANT_Class_info项常量项的结构可知其index数据项又是一个索引项,指向全限定名常量项索引,index数据项的值为0x11=17,0x12=18,表示指向第17、18个常量项,根据constant_pool_count的值为19可得,常量池中一共有18个常量项。根据前面javap的查看结果,第17和第18个常量分别为

如果单纯从字节码找,要知道18个常量项必须知道前17个常量项所占字节,这里就不一一找了,最后找到第18个常量项CONSTANT_Utf8_info在class文件中包含的内容如下:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

CONSTANT_Utf8_info结构如下:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

根据tag等于1得第18项是CONSTANT_Utf8_info型,该类型存储UTF-8编码的字符串,在TestClass.class文件种该常量项种个数据项的内容如下:

  • length(u2):表示UTF-8编码的字符串占用的字节数,值为0x0010=16.

  • bytes(u1):表示长度为length的UTF-8编码的字符串.

  • 因为length=16,所以 length后面紧跟的长度为16个字节的连续数据是一个使用UTF-8缩略编码表示的字符串。后面紧跟的第一个字节为0x6A=106,那该编码代表的字符为j,我们发现106其实就是字符j对应的ASCII码。后面16个字节代表的字符就是: java/lang/Object

UTF-8缩略编码与普通UTF-8编码的区别是:从’\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,从’\u0800’到’\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

关于常量池中的如“I”、 “V”、 “<init>”、 “LineNumberTable”、“LocalVariableTable”等 并没有在代码中出现过,这部分自动生成的常量的确没有在Java代码里面直接出现过,但它们会被后面即将讲到的字段表(field_info)、 方法表(method_info)、 属性表(attribute_info)引用到,它们会用来描述一些不方便使用“固定字节”进行表达的内容。譬如描述方法的返回值是什么?有几个参数?每个参数的类型是什么?因为Java中的“类”是无穷无尽的,无法通过简单的无符号字节来描述一个方法用到了什么类,因此在描述方法的这些信息时,需要引用常量表中的符号引用进行表达。

1.4 访问标志

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

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

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

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

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

接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中

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

1.6 字段表集合

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

描述符的作用是用来描述字段的数据类型、 方法的参数列表(包括数量、 类型以及顺序)和返回值。 根据描述符规则,基本数据类型(byte、 char、 double、 float、 int、 long、 short、 boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。描述符标识字符含义如下表所示:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

1.7 方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、 名称索引(name_index)、 描述符索引(descriptor_index)、 属性表集合(attributes)几项。

1.8 属性表集合

属性表不要求有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的。 下面就几个比较重要的属性做简要介绍:

1.8.1 Code属性

Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。Code属性表的结构如下:

《深入理解Java虚拟机读书笔记(8): 深入理解类文件结构》

  • attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度
  • max_stack代表了操作数栈(Operand Stacks)深度的最大值。 在方法执行的任意时刻,操作数栈都不会超过这个深度。
  • max_locals代表了局部变量表所需的存储空间。 单位为Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、 char、 float、 int、 short、 boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。
  • code_length和code用来存储Java源程序编译后生成的字节码指令。 code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。 code_length是一个u4类型的长度值,理论上最大值可以达到232-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,即它实际只使用了u2的长度。如果超过这个限制,Javac编译器也会拒绝编译。

1.8.2 Exceptions 属性

Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。

1.8.3 LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。

1.8.4 ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。 只有被static关键字修饰的变量(类变量)才可以使用这项属性。

参考

  1. 深入理解java虚拟机
  2. Class文件结构–常量池(一)
    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/wen_fei/article/details/86373936
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞