Class类文件结构
本章说一下Java编译后的class文件结构。
魔数与Class文件的版本
我这里用sublime打开一个class文件,看到前面4个字节是十六进制0xCAFEBABE,这个是Class文件的魔数.
很多文件存储标准中都使用魔数进行身份识别,因为扩展名可以更改,魔数就是确定这个文件是否为一个能被虚拟机接受的Class文件。
然后看0000 0034,转换成十进制是52,这个表示Java编译的版本号,相信大家在工作中也遇见过Unsupported major.minor version 52.0
之类的错误,指的就是这个版本号,52对应的是JDK8。
常量池
再后面的就是常量池,常量池可以理解为Class文件之中的资源仓库,我们前面提到过,Java运行时内存区域里有一块方法区,方法区里面有一个运行时常量池,Class文件的这部分数据,会在运行时被加载到方法区的运行时常量池中。
常量池中主要存放两大类常量:字面量和符号引用。『字面量』比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而『符号引用』则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
先放一段代码
1 | public class TestClass { |
我们用javap
命令来看一下编译后的class文件
1 | ~ javap -verbose TestClass |
Utf8
就是UTF-8编码的字符串,Class
、Methodref
和Fieldref
则是符号引用。符号引用后面的编号最终也指向了字符串表示他们的值。
访问标志
常量池结束后,紧接着2个字节代表访问标志(access_flag)。包括这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final。上面的flags中的值是flags: ACC_PUBLIC, ACC_SUPER表示这个类是public的。
描述符
这里说一下方法和字段的描述符。
基本类型是取首字母的大写基本上。例如byte就是B,有3个特殊的,long是J,boolean是Z,V是void。
L表示对象(例如Ljava/lang/String)。对数组而言,每一维度将使用一个前置的“[”字符来描述。
如定义一个为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String”。
描述方法的时候,是先参数列表,后返回值。参数列表在小括号“()”内。例如()V
表示0个参数,返回值为void,int test(int[] i, char c)
的描述符为([IC)I
。
字节码指令集
aload_0、iconst_1之类的都是字节码指令,下面将字节码操作按用途分为9类,按照分类介绍一下。
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输:
- 将一个局部变量加载到操作栈:iload、iload_
、lload、lload_ 、fload、fload_ 、dload、dload_ 、aload、aload_ - 将一个数字从操作数栈存储到局部变量表:istore、istore_
、lstore、lstore_ 、fstore、fstore_ 、dstore、dstore_ 、astore、astore_ - 将一个常量加载到操作栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_、lconst_
、fconst_ 、dconst_ - 扩充局部变量表的访问索引的指令:wide。
存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作,除此之外,还有少量指令,如访问对象的字段或数组元素的指令也会向操作数栈传输数据。上面有尖括号的表示一组指令(例如iload_
_1、iload_2、iload_3),iload_0也等价于iload 0。
运算指令
运算或算术指令用于堆两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。大体上算术指令可以分为两种:对整型数据进行运算的指令与堆浮点型数据进行运算的指令,无论是哪种算术指令,都使用Java虚拟机的数据类型,由于没有直接支持byte、short、char和boolean类型的算术指令,对于这类数据的运算,应使用操作int类型的指令代替。
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 取反指令:inge、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位与指令:iand、land
- 按位异或指令:ixor、lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显式类型转换操作,或者用于处理字节指令集中数据类型相关指令无法与数据类型一一对应的问题。
以下是宽化类型转换,Java虚拟机直接支持,无需指令:
- int类型到long、float或者double类型
- long类型到float、double类型
- float类型到double类型
窄化类型指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、和d2f。窄化类型转换可能导致不同的正负号、不同的数量级以及精度丢失的情况。
对象创建与访问指令
- 创建类实例的指令:new
- 创建数组的指令:newarray、anewarray、multianewarray
- 访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
操作数栈管理指令
- 将操作数的组合暂定一个或两个元素出栈:pop、pop2
- 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup_x2、dup_x2、dup2_x2
- 将栈最顶端的两个数值互换:swap
控制转移指令
- 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq、和if_acmpne
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
方法调用和返回指令
- invokevirtual调用对象的实例方法
- invokeinterface调用接口方法
- invokespecial调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法
- invokestatic调用类方法
- invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法
异常处理指令
throw语句由athrow指令实现,而catch语句不是由字节码来实现的,采用异常表来实现。
同步指令
同步一段指令集序列在Java语言中是由synchronized语句块来表示的,在Java虚拟机的指令集中由monitorenter和monitorexit两条指令来支持。