Class类文件结构与字节码指令
一、Class类文件结构
Class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的地排列在文件之中,中间没有添加任何分隔符,这使得整个class文件中存储的内容几乎全部都是程序的必要的数据。当遇到需要占用8字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8字节进行存储。
Class文件格式只有俩种数据类型:“无符号数”和“表”。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成的字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性的以“_info” 结尾。表用于描述有层次关系的复合结构的数据,整个class文件本质上也可以是一张表,按严格顺序排列构成。
如下图,为class类结构:
class文件格式:
- 魔数和class文件的版本:每个class文件的头4个字节被称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。紧接着魔数的四个字节存储的是class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。Java的版本号是从第45开始的。
- 常量池,紧接着主、次版本号之后的是常量池的入口,常量池可以比喻成class文件里面的源仓库,它是class文件结构中与其他项目关联最多的数据,通常也是占用class文件空间最大的数据项目之一,另外,他还是class文件中第一个出现的表类型的数据项目。常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。这个容量的计数是从1开始的不是从0开始。常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。符号引用则包括下面几类常量:
- 被模块导出或者开放的包
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
- 方法句柄和方法类型
- 动态调用点和动态常量
常量池中每一项常量都是一个表,截至到jdk13,常量表中分别有17种不同类型的常量。
- 访问标志(access_flag):在常量池结束之后,紧接着的2个字节代表访问标志,这个表示用于是被一些类或者接口层次的访问信息,包括:这个class是类还是接口;是否定义为public;是否定义为abstract类型,等等。access_flag一共有16种标志位可以使用,当前只定义了9个,没有使用的标志位一律为0。
- 类索引(this_class)、父类索引(super_class)与接口索引集合(interfaces);类索引和父类索引都是一个u2类型的数据集合,接口索引集合是一组u2类型的数据集合,class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字后的接口顺序从左到右排列在接口索引集合中。
- 字段表(field_class)用于描述接口或者类中声明的变量。包括类级别变量和实例级别的变量,但不包括在方法内部申明的局部变量。字段可以包括的修饰符有字段的作用域(public、protect)、实例变量还是类变量(static)、可变性(final)、并发可见性(volatile,是否从主内存读写)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)。上面各个修饰符要么有,要么没有,很适合使用标志位来表示。而字段和字段类型,只能引用常量池中的常量来描述。跟随着access_flag的标志的是两项索引值:name_index和description_index。它们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。全限定名:类似:org/test/testclass;简单名称就是指没有类型和参数修饰的方法或者字段名称:类似 inc() inc、字段m m;方法和字段的描述符比较复杂
基本类型以及代表无返回值的void类型都用一个大写的字符表示,而对象则使用字段L加对象的全限定名来表示。对于数组,每一个维度将使用一个前置的[字符来描述,例如:java.lang.String[][] -> [[Ljava.lang.String; 用来描述方法时,按照先参数列表后返回值的顺序描述,例如:int indexof(char[] source, int first) ->([CI)I。字段表集合不会列出从父类或者父接口继承而来的字段,但有可能出现Java代码不存的字段。
- 方法表描述;class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一样的方式,方法表的结构如同字段表一样,依次包括访问标志、名称索引、描述符索引、属性表集合。如果父类方法在子类中没有重写,方法表集合中就不会出现来自父类的方法信息。有可能出现编译器自己的方法.
- 属性表:class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景的专用信息。下面为部分属性表信息。
二、字节码与数据类型
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数据(称为操作码)以及跟随其后的零至多个代表此操作所需的参数(称为操作数)构成。由于Java虚拟机采用面向操作数栈而不是面向寄存器的架构,所以大多数指令都不包括操作数,只有一个操作码,指令参数都放在操作数栈中。Java虚拟机的操作码为一个字节(0-255),这意味着指令集的操作码总数不能超过256条。class文件格式放弃了编译后代码的操作数对齐,这就意味着虚拟机在处理那些超过一个字节的数据时,不得不在运行时从字节中重建出具体的数据结构。
如下为Java虚拟机指令集支持的数据类型。
- 加载与存储指令:用于将数据在栈桢中的局部变量和操作数栈之间来回传输。例如:iload(将一个局部变量加载到操作数栈)、istore(将一个数值从操作数栈存到局部变量表)、bipush(将常量加载到操作数栈)
- 运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入操作数栈顶。例如:iadd、isub、imul、idiv、irem、ineg。
- 类型转换指令:可以将两种不同的数值类型相互转换。
- 对象创建与访问指令:虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建之后,就可以使用对象访问指令获取对象实例的字段或者数组元素
- 创建类指令:new;创建数组指令:newarray,anewarray,multianewarray
- 访问类字段和实例字段:getfield,putfield,getstatic,putstatic
- 把一个数组元素加载到操作数栈的指令:baload,calod,等等
- 将一个操作数栈的元素存储到数组元素的指令:bastore,castore等等
- 取数组长度:arraylength;检查类实例类型的指令:instanceof,checkcast;
- 操作数栈指令:出栈(pop)、互相(swap)
- 控制转移指令:ifeq、iflt等等
- 方法调用和返回指令;invokevirtual(调用对象实例方法,根据对象的实际类型进行分配)、invokeinterface(调用接口方法,在运行时找一个实现了这个接口方法的对象)、invokespecoal(特殊处理的实例方法,类似私用方法、父类方法、初始化方法)、invokestatic(类静态方法)、invokedynamic(运行时动态解析出调用点限定符所引用的方法)。其分配逻辑由用户所设定的引导方法设定。返回指令:ireturn
- 异常处理指令:Java虚拟机中处理异常采用异常表来完成。
- 同步指令:Java虚拟机支持方法级别和方法内部一段指令序列的同步,这俩种都是使用monitro来实现的,同步一段指令序列通常由java语言中的synchronized语句块来表示,Java虚拟机中的指令有monitorenter和monitorexit来支持synchronized的语义。
参考:
《深入理解Java虚拟机》周志明