一概述
本文属于《深入理解java虚拟机》读书笔记,第六章类文件结构。本章主要包含两个部分的内容:class类文件结构与字节码指令简介。下面分别整理。
二class类文件结构
2.1 平台无关性
Java是与平台无关的语言,这得益于Java源代码编译后生成的存储字节码的文件,即Class文件,以及Java虚拟机的实现。不仅使用Java编译器可以把Java代码编译成存储字节码的Class文件,使用JRuby等其他语言的编译器也可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是什么语言,只要它符合一定的结构,就可以在Java中运行。Java语言中的各种变量、关键字和运算符的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更强大。
2.2 Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1、2、4、8个字节的无符号数。表是由多个无符号数或其他表作为数据项构成的符合数据类型,所有的表都习惯性地以“_info”结尾。整个Class文件本质上就是一张表,它由如下所示的数据项构成。
从表中可以看出,无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的该数据项的形式,称这一系列连续的摸一个类型的数据为某一类型的集合,比如,fields_count个field_info表数据构成了字段表集合。这里需要说明的是:Class文件中的数据项,都是严格按照上表中的顺序和数量被严格限定的,每个字节代表的含义,长度,先后顺序等都不允许改变。
下表列出了Class文件中各个数据项的具体含义:
2.2.1魔数与class文件的版本
每个Class文件的头4个字节称为魔数(magic),它的唯一作用是判断该文件是否为一个能被虚拟机接受的Class文件。它的值固定为0xCAFEBABE。紧接着magic的4个字节存储的是Class文件的次版本号和主版本号,高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件。
2.2.2 常量池
版本号之后是常量池(constant_pool)的入口,它是Class文件中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:
类和接口的全限定名(即带有包名的Class名,如:org.lxh.test.TestClass)字段的名称和描述符(private、static等描述符)方法的名称和描述符(private、static等描述符)。虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。
这里说明下符号引用和直接引用的区别与关联:
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。常量池中的每一项常量都是一个表,共有11种(JDK1.7之前)结构各不相同的表结构数据,没中表开始的第一位是一个u1类型的标志位(1-12,缺少2),代表当前这个常量属于的常量类型。11种常量类型所代表的具体含义如下表所示:
jdk1.7为了更好的支持动态语言调用,又增加了3种:CONSTANT_MethodHandle_info(表示方法句柄),CONSTANT_MethodType_info(标识方法类型),CONSTANT_InvokeDynamic_info(表示一个动态方法调用点)。
这14种常量类型均有各自的结构,参照原书,这里不一一列举出来。
2.2.3访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flag),这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口,是否定义为public类型,abstract类型,如果是类的话,是否声明为final,等等。每种访问信息都由一个十六进制的标志值表示,如果同时具有多种访问信息,则得到的标志值为这几种访问信息的标志值的逻辑或(|),可以结合书上的例子来看。
2.2.4类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)则是一组u2类型的数据集合,Class文件中由这三项数据来确定这个类的继承关系。类索引、父类索引和接口索引集合都按照顺序排列在访问标志之后,类索引和父类索引两个u2类型的索引值表示,它们各自指向一个类型为COMNSTANT_Class_info的类描述符常量,通过该常量中的索引值找到定义在COMNSTANT_Utf8_info类型的常量中的全限定名字符串。而接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是个接口,则应当是extend语句)后的接口顺序从左到右排列在接口的索引集合中。
2.2.5字段表集合
字段表(field_info)用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内声明的变量。字段的名字、数据类型、修饰符等都是无法固定的,只能引用常量池中的常量来描述。下面是字段表的最种格式:
原书对于字段进行展开描述,这里不展开。
需要注意一点:字段表集合中不会列出从父类或接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
2.2.6方法表集合
方法表(method_info)的结构与属性表的结构相同,不过多赘述。方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里,关于属性表的项目,同样会在后面详细介绍。
与字段表集合相对应,如果父类方法在子类中没有被覆写,方法表集合中就不会出现来自父类的方法信息。但同样,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<cinit>”方法和实例构造器“<init>”方法。
在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。
2.2.7 属性表结合
属性表(attribute_info)在前面已经出现过多系,在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
原书介绍的比较多,这里简单列出两个。
1)code属性
前面已经说过,Java程序方法体中的代码讲过Javac编译后,生成的字节码指令便会存储在Code属性中,但并非所有的方法表都必须存在这个属性,比如接口或抽象类中的方法就不存在Code属性。如果方法表有Code属性存在,那么它的结构将如下表所示:
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的名称。attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。
max_stack代表了操作数栈深度的最大值,max_locals代表了局部变量表所需的存储空间,它的单位是Slot,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用。
code_length和code用来存储Java源程序编译后生成的字节码指令。code用于存储字节码指令的一系列字节流,它是u1类型的单字节,因此取值范围为0x00到0xFF,那么一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中200条编码值对应的指令含义。code_length虽然是一个u4类型的长度值,理论上可以达到2^32-1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过了这个限制,Javac编译器将会拒绝编译。
在class文件中,Code属性属于描述代码,所有其它数据项目属于元数据(metadata,包含类,字段,方法定义及其他信息).
总结:
关于class类文件,上面只是简单列一些,详细还是看《java虚拟机规范》,但是以上概念有助于分析,但是从常用还是使用javap -verbose *.class。能理解这些编译后的文件就可以了,没必要去直接打开class文件分析。
三 字节码指令
3.1字节码指令简介
Java虚拟机指令由一个字节长度的、代表某种特定含义的操作码(Opcode)以及其后的零个至多个代表此操作参数的操作数构成。虚拟机中许多指令并不包含操作数,只有一个操作码。字节码指令流是单字节对齐,只有"tableswitch"和"lookupswitch"两指令例外,它们的操作数比较特殊,以4字节为界限划分的,需要预留出相应的空位来实现对齐。
对于大部分与数据类型相关的字节码指令,他们的操作码助记符都有特殊的字符来表明:i代表int类型,l代表long,s代表short,b代表 byte,c代表char,f代表float,d代表double,a代表reference。有一些单独指令可以在必要的时候用来将一些不不支持的类型转换为可被支持的类型。
3.2加载和存储指令
加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传输。
1)将一个局部变量加载到操作数栈的指令包括:iload,iload_<n>,lload、lload_<n>、float、 fload_<n>、dload、dload_<n>,aload、aload_<n>。
2)将一个数值从操作数栈存储到局部变量标的指令:istore,istore_<n>,lstore,lstore_<n>,fstore,fstore_<n>,dstore,dstore_<n>,astore,astore_<n>
3)将常量加载到操作数栈的指令:bipush,sipush,ldc,ldc_w,ldc2_w,aconst_null,iconst_ml,iconst_<i>,lconst_<l>,fconst_<f>,dconst_<d>
4)局部变量表的访问索引指令:wide
一部分以尖括号结尾的指令代表了一组指令,如iload_<i>,代表了iload_0,iload_1等,这几组指令都是带有一个操作数的通用指令。
3.3运算指令
算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
1)加法指令:iadd,ladd,fadd,dadd
2)减法指令:isub,lsub,fsub,dsub
3)乘法指令:imul,lmul,fmul,dmul
4)除法指令:idiv,ldiv,fdiv,ddiv
5)求余指令:irem,lrem,frem,drem
6)取反指令:ineg,leng,fneg,dneg
7)位移指令:ishl,ishr,iushr,lshl,lshr,lushr
8)按位或指令:ior,lor
9)按位与指令:iand,land
10)按位异或指令:ixor,lxor
11)局部变量自增指令:iinc
12)比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp
Java虚拟机没有明确规定整型数据溢出的情况,但规定了处理整型数据时,只有除法和求余指令出现除数为0时会导致虚拟机抛出异常。
3.4类型转换指令
java虚拟机对于宽化类型转换直接支持,并不需要指令执行,包括:
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
窄化类型转换指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。但是窄化类型转换很可能会造成精度丢失。
3.5对象创建于访问指令
虽然类实例和数组都是对象,Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。
1)创建实例的指令:new
2)创建数组的指令:newarray,anewarray,multianewarray
3)访问字段指令:getfield,putfield,getstatic,putstatic
4)把数组元素加载到操作数栈指令:baload,caload,saload,iaload,laload,faload,daload,aaload
5)将操作数栈的数值存储到数组元素中执行:bastore,castore,castore,sastore,iastore,fastore,dastore,aastore
6)取数组长度指令:arraylength
7)检查实例类型指令:instanceof,checkcast
3.6操作数栈管理指令
Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2和swap;
3.7控制转移指令
让JVM有条件或无条件从指定指令而不是控制转移指令的下一条指令继续执行程序。控制转移指令包括:
1)条件分支:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnotnull,if_cmpeq,if_icmpne,if_icmlt,if_icmpgt等。
2)复合条件分支:tableswitch,lookupswitch。
3)无条件分支:goto,goto_w,jsr,jsr_w,ret。
JVM中有专门的指令集处理int和reference类型的条件分支比较操作,为了可以无明显标示一个实体值是否是null,有专门的指令检测null 值。boolean类型和byte类型,char类型和short类型的条件分支比较操作,都使用int类型的比较指令完成,而 long,float,double条件分支比较操作,由相应类型的比较运算指令,运算指令会返回一个整型值到操作数栈中,随后再执行int类型的条件比较操作完成整个分支跳转。各种类型的比较都最终会转化为int类型的比较操作。
3.8方法调用和返回指令
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。
invokedynamic指令用于在调用时动态解析出调用点限定符所引用的方法。
而方法返回指令则是根据返回值的类型区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用。
3.9抛出异常指令
athrow
3.10同步
JVM支持方法级同步和方法内部一段指令序列同步,这两种都是通过moniter实现的。
方法级的同步是隐式的,无需通过字节码指令来控制,它实现在方法调用和返回操作中。虚拟机从方法常量池中的方法标结构中的 ACC_SYNCHRONIZED标志区分是否是同步方法。方法调用时,调用指令会检查该标志是否被设置,若设置,执行线程持有moniter,然后执行方法,最后完成方法时释放moniter。
同步一段指令集序列,通常由synchronized块标示,JVM指令集中有monitorenter和monitorexit来支持synchronized语义。
结构化锁定是指方法调用期间每一个monitor退出都与前面monitor进入相匹配的情形。JVM通过以下两条规则来保证结结构化锁成立(T代表一线程,M代表一个monitor):
1)T在方法执行时持有M的次数必须与T在方法完成时释放的M次数相等
2)任何时刻都不会出现T释放M的次数比T持有M的次数多的情况