深入理解java虚拟机----第六章 类文件结构

 

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

6.1 概述

由于最近十年内虚拟机以及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二进制本地机器码(Native Code)已不再是唯一的选择,越来越多的程序语言选择了操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。

6.2 无关性的基石

Java刚诞生的宣传口号:一次编写,到处运行(Write Once, Run Anywhere)。其最终实现在操作系统的应用层:Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码。字节码(ByteCode)是构成平台无关的基石;另外虚拟机的语言无关性也越来越被开发者所重视,JVM设计者在最初就考虑过实现让其他语言运行在Java虚拟机之上的可能性,如今已发展出一大批在JVM上运行的语言,比如Clojure、Groovy、JRuby、Jython、Scala;实现语言无关性的基础仍是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定,它只与Class文件这种特定的二进制文件格式所关联,这使得任何语言的都可以使用特定的编译器将其源码编译成Class文件,从而在虚拟机上运行。

6.3 Class类文件的结构

class文件中只有两种数据类型:无符号数和表。

以u1、u2、u4和u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数

6.3.1 魔数与Class文件的版本

Class文件的头4个字节,唯一作用是确定文件是否为一个可被虚拟机接受的Class文件,固定为“0xCAFEBABE”。

第5和第6个字节是次版本号,第7和第8个字节是主版本号(0x0034为52,对应JDK版本1.8);能向下兼容之前的版本,无法运行后续的版本;

6.3.2常量池

常量池可以理解为Class文件之中的资源仓库,是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项之一;

由于常量池中的常量数量不固定,因此需要在常量池前放置一项u2类型的数据来表示容量,该值是从1开始的,上图的0x0013为十进制的19,代表常量池中有18项常量,索引值范围为1~18;

常量池主要存放两大类常量:字面量(Literal,笔记接近Java的常量概念,比如文本字符串和final常量等)和符号引用(Symbolic References,主要包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符);

Java代码在javac编译时不会有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接;所以在Class文件不会保存各个方法、字段和最终内存布局信息;当虚拟机运行时需要从常量池获取对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中;

JDK 1.7中常量池共有14种不同的表结构数据,这些表结构开始的第一位是一个u1类型的标志位,代表当前常量的类型,具体如下图所示:

每一项的内容自己查书吧 感觉没什么意义都学出来

6.3.3 访问标志

紧接在常量池后面的是两个字节的访问标志,用于标识类或接口的访问信息;

访问标志一个有16个标志位,但目前只采用了其中8位,本例子中的0x0021标识为一个public的普通类;

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

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合。Class中这三项数据来确定这个类的继承关系。

对于接口索引集合,入口第一项——u2类型的数据为接口计数器 之后为接口。

6.3.5字段表集合

用于描述接口或者类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量;它不会列出从父类和超类继承而来的字段;

0x0001表示这个类只有一个字段表数据;

字段修饰符放在access_flag中,是一个u2的数据类型,0x0002表示为private的属性;

字段名称name_index,是一个u2的数据类型,0x0005表示该属性的名称为常量池的第5项;

字段描述符descriptor_index,是一个u2的数据类型,0x0006表示该属性的描述符为常量池的第6项,其值“I”表示类型为整形;

字段属性计算器和属性集合:0x0000表示该例子中不存在;

6.3.6 方法表集合

和字段表集合的方式几乎一样;

方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面;

0x0002表示这个类有两个方法表数据,分别是编译器添加的实例构造器<init>和源码中的方式inc();

第一个方法的访问标志是0x0001(public方法),名称索引值为0x0007(常量池第7项,“<init>”),描述符索引值为0x0008(常量池第8项,“()V”),属性表计算器为0x0001(有一项属性),属性名称索引为0x0009(常量池第9项,“Code”);

根据“6.3.7.1 Code属性”说明,属性值的长度为23(0x0000001D表示29,但需要减去属性名称索引和属性长度固定的6个字节长度),操作数栈深度的最大值为1(0x0001,虚拟机运行时根据这个值来分配栈帧中操作栈深度),局部变量表所需要的存储空间为1个Slot(0x0001,Slot是内存分配的最小单位),字节码长度为5(0x00000005),分别为2A(aload_0,将第0个Slot中为reference类型的本地变量推送到操作数栈顶)、B7(invokespecial,以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它父类的方法,后面接着一个u2的参数指向常量池的方法引用)、0x0001(表示常量池的第1项,即Object类的<init>方法)、B1(对应的指令为return,返回值为void);显式异常表为空(0x0000,计数器为0);该Code属性还内嵌1个属性(0x0001),属性的名称索引为0x000A(即“LineNumberTable”属性,用于记录对应的代码行数),该内嵌属性的长度为6(0x00000006),对应的行数信息为源码的第3行(0x000100000003);

第二个方法的访问标志是0x0001(public方法),名称索引值为0x000B(常量池第11项,“inc”),描述符索引值为0x000C(常量池第12项,“()I”),属性表计算器为0x0001(有一项属性),属性名称索引为0x0009(常量池第9项,“Code”);

根据“6.3.7.1 Code属性”说明,属性值的长度为25(0x0000001F表示31,但需要减去属性名称索引和属性长度固定的6个字节长度),操作数栈深度的最大值为2(0x0002),局部变量表所需要的存储空间为1个Slot(0x0001),字节码长度为7(0x00000007),分别为2A(aload_0)、B4(getfield,后面接着一个u2的参数指向常量池的属性引用)、0x0002(表示常量池的第2项,即TestClass类的m属性)、04(对应的指令为iconst_1)、60(对应的指令为iadd,整形求和)、AC(对应的指令为ireturn,返回值为整形);显式异常表为空(0x0000,计数器为0);该Code属性还内嵌1个属性(0x0001),属性的名称索引为0x000A(即“LineNumberTable”属性,用于记录对应的代码行数),该内嵌属性的长度为6(0x00000006),对应的行数信息为源码的第8行(0x000100000008);

6.3.7 属性表集合

在Class文件、字段表、方法表都可以携带自己的属性表集合;

属性表集合的限制较为宽松,不再要求严格的顺序,只要属性名不重复即可;

以下是Java虚拟机规范里预定义的虚拟机实现应当能识别的属性:

每一个属性不写了 我也记不住

6.4字节码指令简介

Java虚拟机的指令由一个字节长度的、代表着特定操作含义的数字(操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

在指令集中大多数的指令都包含了其操作所对应的数据类型信息,如iload指令用于从局部变量表中加载int类型的数据到操作数栈中。

加载和存储指令:iload/iload_<n>等(加载局部变量到操作栈)、istore/istore_<n>等(从操作数栈存储到局部变量表)、bipush/sipush/ldc/iconst_<n>(加载常量到操作数栈)、wide(扩充局部变量表访问索引);

运算指令:没有直接支持byte、short、char和boolean类型的算术指令而采用int代替;iadd/isub/imul/idiv加减乘除、irem求余、ineg取反、ishl/ishr位移、ior按位或、iand按位与、ixor按位异或、iinc局部变量自增、dcmpg/dcmpl比较;

类型转换指令:i2b/i2c/i2s/l2i/f2i/f2l/d2i/d2l/d2f;

对象创建与访问指令:new创建类实例、newarray/anewarray/multianewarray创建数组、getfield/putfield/getstatic/putstatic访问类字段或实例字段、baload/iaload/aaload把一个数组元素加载到操作数栈、bastore/iastore/aastore将一个操作数栈的值存储到数组元素中、arraylength取数组长度、instanceof/checkcast检查类实例类型;

操作数栈管理指令:pop/pop2一个或两个元素出栈、dup/dup2复制栈顶一个或两个数组并将复制值或双份复制值重新压力栈顶、swap交互栈顶两个数值;

控制转移指令:ifeq/iflt/ifnull条件分支、tableswitch/lookupswitch复合条件分支、goto/jsr/ret无条件分支;

方法调用和返回指令:invokevirtual/invokeinterface/invokespecial/invokestatic/invokedynamic方法调用ireturn/lreturn/areturn/return方法返回;

异常处理指令:athrow

同步指令:monitorenter/monitorexit

6.5 公有设计和私有实现

Java虚拟机的实现必须能够读取Class文件并精确实现包含在其中的Java虚拟机代码的含义;

但一个优秀的虚拟机实现,通常会在满足虚拟机规范的约束下具体实现做出修改和优化;

虚拟机实现的方式主要有两种:将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集或宿主主机CPU的本地指令集。

6.6 Class文件结构的发展

Class文件结构一直比较稳定,主要的改进集中向访问标志、属性表这些可扩展的数据结构中添加内容;

Class文件格式所具备的平台中立、紧凑、稳定和可扩展的特点,是Java技术体系实现平台无关、语言无关两项特性的重要支柱;

6.7 本章小结

本章详细讲解了Class文件结构的各个部分,通过一个实例演示了Class的数据是如何存储和访问的,后面的章节将以动态的、运行时的角度去看看字节码在虚拟机执行引擎是怎样被解析执行的。

猜你喜欢

转载自blog.csdn.net/qq_40182703/article/details/81208867