第六章 类文件结构
1、无关性的基石
- 各种不同平台的虚拟机与所有平台都统一使用程序存储格式——字节码是构成平台无关的基石。
- 实现语言无关性的基础仍然是虚拟机和字节码存储格式,Java虚拟机不和包括Java在内的任何语言绑定只能与class文件这种特定的二进制文件格式所关联。
- class文件包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
2、class文件结构
概述:
- class文件是一组以八位字节为基础的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件中。
- class文件格式采用一种类似于C语言结构的伪数据结构来存储数据,这种伪数据结构包含两种数据类型无符号和表。
- 无符号数属于基本数据类型,从u1、u2、u4、u8来分别表示一个字节、两个字节、四个字节、八个字节的无符号数。无符号数可以用来描述数字、索引、引用7数量值或者按照utf-8编码构成字符串值。
- 表是由多个无符号或者其他表作为数据项构成的复合数据类型。所有的表都习惯性地以“_info”结尾。表示描述有层次关系的复合结构的数据,整个class文件本质就是一张表。
魔数:
- 每个class文件的头四位为魔数,作用是确定这个文件是否是一个能被虚拟机引用接受的class文件,class文件的魔数值是0xCAFEBABE。
class文件的版本:
- 紧接着魔数的四个字节存储的是class文件的版本号,第5和第6个字节是次版本号,第7和第8个是主版本号。
常量池:
- 紧接着主版本号的是常量池入口,常量池可以理解为class文件中的资源仓库,它是class文件结构中与其他项目关联的最多的数据类型,也是占有class文件空间最大的数据项目之一,同时它是class文件第一个出现的表类型数据。
- 由于常量池的常量数值不固定,所以在常量池的入口需要放置一项u2类型的数据代表常量池容量计数值,计数是从一开始的。
- 常量池主要存放两大常量,字面量和符号引用,字面量比较接近于Java语言层面的常量概念,如文字字符串,声明为final的常量池等。
- 符号引用包括类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
- 常量池中每一项常量都是一个表,根据标志位来确定项目类型。
package com.ecut.clazz; public class TestClass {
private int m; public int inc() {
return m + 1;
}
}字节码文件对应的16进制如下图:
常量容量计数值为22则代表有21个常量,第一个常量的标志位是0x0A,根据下表可知对应的项目类型是CONSTANT_Methodref_info,此类型代表类中方法的符号引用,这个项目类型对应的结构有两个index,第一个index为0x0004为即十进制4,第二个index为0x0012即十进制18。第二个常量是以此类推,这个项目类型为CONSTANT_Fieldref_info,第一个index为0x0003即十进制3,第二个index为0x0013即十进制19。
- 剩下的常量可以借助class文件字节码的工具javap来输出TestClass.class文件字节码内容。采用javap -verbose TestClass.class命令。
访问标志:
- 在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于标识一些类或者接口层次的访问信息,包括是类还是接口,是否为public类型,是否为abstract。具体的标志位以及标志的含义如下表:
- access_flag中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。
- TestClass被public修饰并且使用了jdk1.2之后的编译器进行了编译,因此它的access_flag标志值应该为100001=0x21。
类索引、父类索引、接口索引:
- 类索引用于确定表达这个类的全限定名,父类索引用于确定这个类的父类的全限定名,接口索引集合就用于描述这个类实现了那些接口,这些接口按implements语句从左到右排列在接口缩影集合中。
字段表集合:
- 用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
- 字段集合不会列出从超类中或者父类接口中继承而来的字段,但有可能列出原本没有的字段,如在内部类中为了保持外部类的访问性会加入外部类的实例字段。
- 字段包含信息:字段的作用域(public、private、protected),是实例变量还是类变量(static修饰符),可变性(final)、并发性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符),字段数据类型(基本类型、对象、数组),字段名称。
- 字段修饰符放在access_flag项目中,它包含两项name_index和descript_index,他们都是对常量池的引用,分别代表字段的简单名称和字段的描述符。
- 字段表结构如下图所示:
- 结合之前的常量池中的常量可知类索引为 com/ecut/clazz/TestClass,父类索引为 java/lang/Object。TestClass包含一个被private修饰的int 类型的m。
方法表集合:
- 方法表集合包括访问标志、名称索引、描述符索引、属性集合。如果父类方法在子类方法中没有被重写,方法表集合中不会有来自父类的方法信息。
- 方法表结构如下图:
属性表集合:
- 在class文件中,字段表方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
- 属性集合的结构如下图:
- 结合常量池对文件进行分析可得,方法容量为2包含了两个方法,第一个方法时public修饰void init,该方法包含了一个属性code,code操作数栈最大值为1.第二个方法时public修饰的int inc()该方法也包含了一个属性code。
- Exception属性列举出方法中可能抛出的异常,也就是方法描述时在throws关键字后面列出的异常。
- LineNumberTable属性用于描述Java源码行号和字节码行号之间的对应关系。
- LocalVariableTable属性用来描述栈帧中局部变量与Java源码中定义的变量之间的关系。
- SourceFile属性用来记录生成这个class源码文件名称。
3、字节码指令
概述:
- Java虚拟机的指令由一个字节长度的,代表着某中特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(称为操作数)而构成。
- Java虚拟机采用面向操作数栈的架构所以大多数指令都不包含操作数只有一个操作码。
- 在Java虚拟机的指令集中大多数指令都包含了其操作锁对应的数据类型信息。
加载和存储指令:
- 加载存储指令用来将数据在栈帧中的局部变量和操作数栈之间来回传输。
- 将一个局部变量加载到操作数栈iload。
- 将一个数值从操作数栈中存储到局部变量表istore。
- 将一个常量加载到操作数栈bipush。
- 扩充局部变量表的访问索引的指令wide。
运算或算法指令:
- 运算或算法指令用于对两个操作数栈上的值进行某中特定的运算,并把结果重新存入操作数栈的栈顶。
类型转换指令:
- 类型转换指令可以将两种不同的数值类型进行相互转换,这种转换操作一般用于实现用户代码中的显示类型转换操作。
- int——》long——》float——》double。
对象创建和访问指令:
- 虽然类实例和数组都是对象但是使用不同的指令来完成创建。
- 创建类实例的指令 new。
- 创建数组的指令 newarray。
- 访问类字段和实例字段 putfield 、getfield、getstatic。
操作数栈管理指令:
- 将操作数栈的栈顶一个元素出栈pop。
- 将栈顶两个数值交换swap。
控制转移指令:
- 在有条件或者无条件的修改PC寄存器的值。
- 条件分支ifea、iflt。
- 复合条件分支lookupswich。
- 无条件goto 。
方法调用和返回指令:
- 方法调用指令和返回值无关,而方法返回指令是根据返回值类型来区分的。
- invokevirtual用于调用对象的实例方法。
- invokestatic用于调用类方法。
异常处理指令:
- 在Java程序中显示抛出异常的操作都有athrow指令来完成。
- Java虚拟规范还规定许多运行时异常会在其他指令检测到异常时自动抛出。
同步指令:
- Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
推荐博客链接:
https://blog.csdn.net/u010349169/column/info/jvm-principle
转载请于明显处标明出处: