深入理解java虚拟机(三)----类文件结构

时间:2022-12-28 08:09:31

java虚拟机具备两个特点:
1. 平台无关性:各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码是构成平台无关性的基石
2. 语言无关性:jvm执行的仅是字节码,对于是什么语言转化成的字节码,虚拟机并不在意。比如Java语言中的各种变量,关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力比java语言本身更强大。有一些Java语言本身无法有效支持的语言特性不代表字节码本身也无法支持。


Class文件中存放的便是字节码。Class文件是一组以字节(8位)为基本单位的二进制流。当遇到需要占用8位以上空间的数据项时,则会按照高位在前的方式分割成若干个字节进行存储。
无符号数:以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数。
表:表是由多个无符号数或者其他表作为数据项构成的符合数据类型,整个Class文件本质上就是一张表。
Class文件的结构如下:
深入理解java虚拟机(三)----类文件结构
1. 魔数与class文件的版本:每个Class文件的头四个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准都是用魔数来进行身份识别。Class文件的魔数值为:”0xCAFEBABE”,紧接着4个字节为Class文件的版本号。第五,六个字节为次版本号,第七,八个字节为主版本号。java的主版本从45开始。高版本的JDK可以兼容低版本的Class文件。
下面给出一段代码。后面将以下面代码使用JDK1.8编译输出的Class文件为基础进行讲解。

package org.yjw.clazz;
public class TestClass{
private int m;
public int inc(){
return m+1;
}
}

编译后生成的class文件用vim :%!xxd以16进制打开
深入理解java虚拟机(三)----类文件结构
可以清楚的看见前四个字节表示的正是0xCAFEBABE,代表次版本好为0x0000,主版本号为0x0034,52代表JDK1.8或者以上版本的虚拟机可以执行此class文件。
在javac后可以加-target 规定class文件的版本以使其在低版本虚拟机上运行。
2. 常量池(要注意和运行时常量池的区别):紧接着主次版本号之后的是常量池入口。常量池可以理解为Class文件之中的资源仓库。它是class文件结构中与其他项目关联最多的数据类型。,也是占用class文件空间最大的数据项目之一。同时它还是在class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。本文件中,常量池计数值为0x13,十进制为19。这代表常量池中有18项常量,索引范围1-18(第0项空出)。常量池中主要存放两大类型常量:字面量和符号引用字面量比较接近java语言层面的常量概念,如文本字符串,声明为final的常量池(为什么局部内部类引用的局部变量必须是final类型:因为其存在常量池中(不会在块作用域结束时被回收),不会出现局部变量生命周期结束,对象还未被回收从而引用不存在的变量。)等。而符号常量则属于编译原理方面的概念,包括以下三类常量:类和接口的全限定名(带包名,唯一标识);字段的名称和描述符;方法的名称和描述符。
javac在编译的时候没有像c/c++那样的连接的步骤,而是在虚拟机加载Class文件的时候进行动态链接,也就是说,在Class文件中不会保存各个方法,字段的直接内存入口地址,必须要经过运行期解析(符号引用→直接引用)。所有当虚拟机运行时,需要从常量池获得对象的符号引用,才能在类创建时或运行时解析。
常量池中每一项常量都是一个表,在JDK1.7之前共有11种结构各不相同的表数据结构。这些表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前常量属于哪个类型。常量池的项目类型及对应的标志如下表:
深入理解java虚拟机(三)----类文件结构
回头看我们的class文件:常量池的第一项标志位为0x0a,十进制为10,是一个类中方法的符号引用。由CONSTANT_Methodref_info的结构可知:声明方法的累描述符索引为0x0004(看常量池第四项,为一个类描述符CONSTANT_Class_info 指向全限定名的常量项索引为:0x0012,为”java/lang/Object”),指向名称及类型描述符的索引为0x000f,查看该CONTANT_NameAndType_info类型指向字段或方法名称常量项的索引为:0x0007,为””,指向该字段或方法描述符常量项的索引为:0x0008,为”()V”,所以我们得出常量池的第一项是一个方法的符号引用,值为java/lang/Object.””:()V,可以看出是public从object继承下来的构造方法。其中常量结构(CONSTANT_Utf8_info)的最大长度为65535B。因为length是一个u2类型的无符号数,所以在java中,类名,字段名,方法名不能超过64KB。常量池中的14种常量项的结构如下表所示:
深入理解java虚拟机(三)----类文件结构
oracle提供了javap工具用于分析字节码,使用javap -verbose TestClass.class输出class文件分析结果。
深入理解java虚拟机(三)----类文件结构
3. 访问标志:在常量池结束后(0x00000a0行74(t)处结束),紧接着的两个字节代表访问标志,这个标志用于标识一些类或者接口层次的访问信息。包括这个Class是类是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是否被声明为final等。具体含义见下表:
深入理解java虚拟机(三)----类文件结构
本文中标志位为0x0021=0x0020 | 0x0001
4. 类索引,父类索引与接口索引集合:**类索引(this_class)和父类索引(super_class)都是一个u2类型的数据。他们各自指向一个类型为CONSTANT_Class_info的类描述符常量项。而接口索引集合是一组u2类型的数组集合。第一项为接口计数器,之后指向常量池中的接口名。**Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。除了Object类所有类的父类索引不为0(接口也为0)。接口索引集合表示这个类实现了哪些接口。类就是implements,接口就是extends.
本例中,这一段为0x0003 0004 0000.
5. 字段表(field_info)集合:字段包括类级变量和成员变量。不包括局部变量。字段表结构如下:
深入理解java虚拟机(三)----类文件结构
其中access_flags意义如下:
深入理解java虚拟机(三)----类文件结构
接口中的字段必须含有ACC_PUBLIC,ACC_STATIC,ACC_FINAL标志。
跟随access_flags有两个索引值:name_index和descriptor_index。他们是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。简单名称是没有那些限定的,本例中的字段简单名就是m,方法简单名就是inc。描述符的作用是用来描述字段的数据类型,方法的参数列表(数量,类型,顺序)和返回值。根据描述符规则,基本数据类型(没有String)以及代表无返回值的void类型都用一个大写字母表示。而对象类型则用字符L加上对象的全限定名表示
深入理解java虚拟机(三)----类文件结构
对于数组类型。每一个维度使用一个前置的”[“来描述,如定义一个java.lang.String[][]的二维数组,将被记录为”L[[java.lang.String;”
用描述符描述方法时,按照先参数列表,后返回值的顺序描述参数列表按照参数的严格顺序放到一个小括号呢,如我们的inc方法就是()V,比如本本例中的字段表:0x0001(一个字段) 0002(ACC_PRIVATE) 0005 (字段名常量池中索引为#5 m)0006(描述符索引为#6 I),可以知道private int m
descriptor_index之后跟随者一个属性表用于存储一些额外的信息。如果是final static int m = 123,那么就可能会存在一项名称为ConstantValue的属性,其值指向常量池中的常量123.
字段表集合中不会列出从超类或者父接口继承而来的字段,但有可能列出原java代码不存在的字段,譬如内部类为了保证对外部类的访问性,会自动添加指向外部类实例的字段。对于字节码而言,字段是可以重载的,可以同名不同描述符,但是java不行。
6. 方法表集合:Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一样的方式,方法表结构如下:
深入理解java虚拟机(三)----类文件结构
其中方法访问标志(access_flag)含义如下:
深入理解java虚拟机(三)----类文件结构
本例中方法表的数据为0x0002(2个方法) 0001(ACC_PUBLIC) 0007(””) 0008(”()V”) 0001(1项属性) 0009(”Code”)

方法表中不会有父类的方法,如果你不重写的话。但是会有一些编译器自动添加的方法,比如这里的实例构造器。在Java语言中,要重载一个方法,简单名要相同,特征签名不同(参数列表),仅仅依靠返回值不同,在java语言层面不能重载,但是在Class文件格式中,描述符的不同就可以重载。
7. 属性表集合:在Class文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某种场景专有的信息。在属性表的实现中,只要不与已有属性名重复,可以在编译时添加任何自定义的属性信息,jvm会自动忽略掉它不认识的属性。下表为虚拟机规范预定义的属性:
深入理解java虚拟机(三)----类文件结构
对于每个属性,它的名称都需要从常量池中引用一个CONSTANT_Utf8_info类型来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的字节数即可:
深入理解java虚拟机(三)----类文件结构
下面介绍几个重要的属性:
- Code:Java程序方法体重的代码经过javac编译后,最终变为字节码存放在Code属性中。Code属性出现在方法表的属性集合中,但并非所有方法表都具有这个属性,接口中的或者抽象方法就没有,如果方法表有Code属性存在,那么Code属性表的结构如下:
深入理解java虚拟机(三)----类文件结构
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为Code。attribute_length指定属性长度,属性值的长度应该为属性长度减去属性名称和属性长度的6个字节。max_stack代表操作数栈深度的最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根绝这个值来分配栈帧中的操作栈深度max_locals代表局部变量表所需的存储空间。单位是Slot(最小单位,32位)。方法参数(包括this),显示异常处理器的参数,方法体内的局部变量都需要使用局部变量表来存放。Slot可以重用。javac根据变量的作用于来分配Slot给各个变量使用,然后计算出max_locals大小。code_length,code用来存储java编译后生成的字节码指令。每个指令是一个u1的单字节。jvm目前已经定义了约200条编码值对应的指令含义。
深入理解java虚拟机(三)----类文件结构
接着分析TestClass文件的字节码,现在分析到0x00000ca,attribute_length为0x0000001d,max_stack为0x0001,max_locals为0x0001,code_length为0x00000005,下面就是code了,为0x2a b7 00 01 b1。
1)读入2a,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个slot中为reference类型的本地变量推送到操作数栈顶。
2)读入b7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法的接收者,为了调用此对象的实例构造器方法,private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Mthodref_info类型常量,即符号引用。读入00 01,这是invokespecial的参数,查常量池得java/lang/Object.””:()V。
4)读入B1,查表得0xB1对应的指令为return,若返回值为void,则这条指令执行后,方法结束。
这段字节码虽然很短,但是可以看出,它的执行过程中的数据交换,方法调用等操作都是基于操作数栈的。我们再次使用javap指令把此文件中的另外一个方法的字节码指令计算出来:
深入理解java虚拟机(三)----类文件结构
注意到的args_size和locals为1,是this。因为这是实例方法,需要通过this访问调用此方法所属的对象。在字节码指令之后是这个方法的显示异常处理表集合。本例中没有异常表,异常表的格式如下:
深入理解java虚拟机(三)----类文件结构
原书中给了一个特别好的例子,这个例子我将在异常的深度分析中讲到。
2. Exception属性:列举方法可能throws的异常。
3. LineNumberTable属性:用于描述Java源码行号到字节码行号之间的对应关系。
4. LocalVariableTable属性:用于描述栈帧中局部变量中的变量与Java源码中定义的变量的对应关系
5. SourceFile属性:用记录生成这个Class文件的源码文件名称。
6. ConstantValue属性:虚拟机对于非static变量的复制在方法中进行(不是构造函数!!!!这个方法就算你有了构造函数也一定会被调用,是类加载过程的最后一步,初始化!)。对于类变量,则有两种方式选择,和ConstantValue,要求有了ConstantValue属性的字段必须要设置ACC_STATIC。
7. InnerClasses属性:用于记录内部类和宿主类的关联。
8. Deprecated及Synthetic属性:是否失效和是否由java源码产生。
其余属性表和属性表的具体结构还请参考原书