Java的技术体系包括
- 支持Java程序运行的虚拟机(JVM)
- 提供接口支持的Java API
- Java 编程语言
- 第三方Java框架(如Spring等)
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编程语言的一大步。
java代码经过编译器编译后,类中方法体内的代码逻辑会被编译为一行行虚拟机能够识别的指令,而java虚拟机规范中通过一个字节来存储所有的虚拟机指令,因此我们将这种指令集称之为字节码指令。
字节码指令的含义
字节码指令是指,由一个字节长度来表示,代表着特定含义的数字(称之为操作码)所构成的指令,其后有可能跟随一个或者多个此命令所需要的参数(称之为操作数)。大多数的字节码指令都是不包含操作数的,只存在一个操作码。
字节码指令的优势在于,由于不需要操作数长度对齐,所以可以节省很多用于操作数对齐而带来的填充和间隔符号所占用的空间。同时一个字节的长度来代表操作码,从设计的初衷我们就能看出视为了尽可能获得短小的编译代码。而这样的设计初衷也是由java语言为了尽可能追求在小数据量和高传输效率的场景中发挥优势所决定的。
它的劣势同样十分明显,由于一个字节所能代表的取值范围为0~255,这就意味着操作码最多只能有256个。同时由于没有操作数长度对齐,当操作码需要处理的数据大于一个字节的长度的时候,比如要存储一个16位长度的数据,那需要两个字节来存储,并且必须在运行时通过某种规则还原出原始的数据。这样的操作会导致字节码在执行的时候必然会损耗一些性能。
分类
字节码指令按照用途,大致可以分为9类
- 加载和存储指令
- 运算指令
- 类型转换指令
- 对象创建与访问指令
- 操作数栈管理指令
- 控制转移指令
- 方法调用和返回指令
- 异常处理指令
- 同步指令
加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输,这些指令包括如下
- 将一个局部变量加载到操作数栈:iload, iload_<n>, lload, lload<n>, fload, fload_<n>, dload, dload_<n>, aload, aload_<n>
- 将一个数值从操作数栈存储到局部变量表:istore, istore_<n>, lstore, lstore_<n>, fstore, fstore_<n>, dstore, dstore_<n>, astore, astore_<n>
- 将一个常量加载到操作数栈:bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>
- 扩充局部变量表的访问索引的指令:wide
运算指令
运算指令用于将两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作数栈顶。算数指令大体可以分为两种:对整型数据进行运算的指令,和对浮点型数据进行运算的指令。
- 加法指令:iadd, ladd, fadd, dadd
- 减法指令:isub, lsub, fsub, dsub
- 乘法指令:imul, lmul, fmul, dmul
- 除法指令:idiv, ldiv, fdiv, ddiv
- 求余指令:irem, lrem, frem, drem
- 取反指令:ineg, lneg, fneg, dneg
- 位移指令:ishl, ishr, iushr, lshl, lshr, lushr
- 按位或指令:ior, lor
- 按位与指令:iand, land
- 按位异或指令:ixor, lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg, dcmpl, fcmpg, fcmpl, lcmp
虚拟机规范中规定,在运算指令时,只有在除法指令或者求余指令中当除数为0的时候,会抛出ArithmeticException,其余任何场景即使出现溢出,也不会产生任何运行时异常
类型转换指令
类型转换指令可以将两种不同数值类进行相互转换。java虚拟机直接支持以下类型的宽化类型转换
- int -> long float double
- long -> folat double
- float -> double
对于处理窄化类型转换时,必须显式地使用转换指令来完成。这些指令包括:i2b, i2c, i2s, f2i, f2l, d2i, d2l, d2f。数据类型窄化处理可能会出现溢出和精度丢失等问题,但是java虚拟机规范中规定,数值窄化处理不会抛出任何运行时异常。
对象创建与访问指令
虽然在java语言层面,类实例和数组都是对象,但是虚拟机对类实例和数组的创建和访问使用的是不同的指令,这是因为他们的创建过程是不同的。指令如下
- 创建类实例的指令: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_1, dup2_1, dup_2, dup2_2
- 将栈最顶端的两个数值互换:swap
控制转移指令
控制转移指令可以让虚拟机有条件或者无条件地从指定位置继续执行程序。指令如下
- 条件分支:ifeq, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmpgt, if_icacmpeq, if_acmpne
- 复合条件分支:tableswitch, lookupswitch
- 无条件分支:goto, goto_w, jsr, jsr_w, ret
方法调用和返回指令
- invokevirtual, 用于调用对象的实例方法,根据对象的实际类型进行分派
- invokeinterface, 用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用
- invokespecial, 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法、父类方法
- invokestatic, 用于调用类方法(static方法)
- invokedynamic, 用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
- 方法返回指令:return, ireturn, lreturn, freturn, dreturn, areturn
异常处理指令
java程序中,显式抛出异常的操作都是由athrow指令来实现。除此之外,虚拟机规范还规定了许多运行时异常会由虚拟机自动抛出。
同步指令
java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,都是使用管程(Monitor)来支持。方法级的同步是隐式的,无需通过字节码指令来控制,因为通过方法常量池的方法表结构中的 ACC_SYNCHRONIZED 访问标志可以得知一个方法是否声明为同步方法。方法内部的同步则是通过 monitorenter 和 monitorexit 两条指令来完成。编译器需要确保,方法中调用过的每条 monitorenter 指令都必须执行其对应的 monitorexit指令。
总结
Java虚拟机规范规定了Java虚拟机应共同遵守的存储格式:Class文件格式 和 字节码指令集。Class文件结构从虚拟机规范发布以来,java的技术体系发生了很多很多的变化,包括语言,API等有了极大的发展和很多的变化。但Class文件结构一直处于比较稳定的状态,它的主体结构、字节码指令都几乎没有什么大的变化。这也说明了反应出虚拟机的具体实现和上层的使用之间的耦合性很低,灵活性很大。
虚拟机加类加载机制的前三篇文章,详细阐述了Class文件的组成部分,每部分的含义,结构,以及使用方法。因为它是虚拟机执行引擎的数据入口,也是Java技术体系最重要的基础构成之一。
Class文件格式所具备的平台中立、紧凑、稳定、可扩展的特点,是Java技术体系能够实现平台无关和语言无关的重要支柱