java虚拟机规范(se8)——java虚拟机结构(六)
2.11 指令集简介
java虚拟机指令由一个字节的操作码,后面跟着多个0个或多个操作数组成,操作码描述了执行的操作,操作数提供了操作所需的参数或者数据。许多指令没有操作数只包含一个操作码。
如果忽略异常处理,那java虚拟机使用下面的伪代码循环即可有效工作:
1 do{ 2 自动计算pc然后获取pc中的操作码; 3 if (存在操作数) 4 获取操作数; 5 执行这个操作码定义的操作 6 }while(是否有更多需要执行);
操作数的数量和大小都有操作码决定。如果一个操作数大于一个字节,那么它将以大端顺序存储——高位在前。例如一个16位无符号整数使用两个无符号字节存储,byte1和byte2,它的值位(byte1<<8)|byte2.
字节码指令流是单字节对齐的,除了lookupswitch和tableswitch指令。由于它们的操作数比较特殊,都是4字节为界划分的,所以这两条指令也需要预留响应的空位来实现对齐。
2.11.1 类型和java虚拟机
java虚拟机指令集中的大多数指令都包含了其操作数的类型信息。例如,iload指令加载一个局部变量到操作数栈中,这个局部变量必须是int类型。fload指令对float值执行相同的操作。这两个指令可以具有相同的实现,但是不能使用相同的操作码。
对于大多数类型相关的指令,指令类型在操作码助记符中用一个字母显示的表示:i表示int,l表示long,s表示short,b表示byte,c表示char,f表示float,d表示double,a表示reference。一些指令的类型是不确定的,这些指令的助记符中不包含类型字母。例如,arraylength总是操作数组对象。一些指令,例如goto是一个无条件控制转移指令,不操作一个有类型的操作数。
由于java虚拟机的操作码长度只有一个字节,所以包含了数据类型的操作码对指令集的设计带来很大的压力。如果每个类型指令都支持所有的java虚拟机运行时数据类型,那么只用一个字节表示所有的指令就不太可能了。相应的,java虚拟机的指令集对于特定的操作提供了有限的类型支持,换句话说,不是每种类型的每个操作都有对应的类型指令。根据需要,可以使用单独的指令在不支持和支持的数据类型之间进行转换。
下表汇总了java虚拟机的指令集支持的类型。具体的指令具有类型信息,通过将opcode列的指令模板中的字母T替换为对应类型列的字母来得到。如果一些指令模板对应的类型列是空的,就表示不存在支持这个类型操作的指令。例如,存在加载int类型的指令iload,但是没有加载byte的指令。
注意到表中的大部分指令都没有整数类型byte,char和short的形式,也没有一个有boolean类型的形式。编译器在编译期和运行期将byte和shot类型带符号位扩展为int类型,将boolean和char类型进行零扩展为int类型,从而使用int类型指令进行操作。同样的,使用java虚拟机指令处理boolean,byte,char,short类型的数组时也会转换为int类型。因此,大多数对于boolean,byte,short和char类型的操作,实际上都是作为int类型作为运算(computational )类型。
opcode | byte | short | int | long | float | double | char | reference |
---|---|---|---|---|---|---|---|---|
Tipush | bipush | sipush | ||||||
Tconst | iconst | lconst | fconst | dconst | aconst | |||
Tload | iload | lload | fload | dload | aload | |||
Tstore | istore | lstore | fstore | dstore | astore | |||
Tinc | iinc | |||||||
Taload | baload | saload | iaload | laload | faload | daload | caload | aaload |
Tastore | bastore | sastore | iastore | lastore | fastore | dastore | castore | aastore |
Tadd | iadd | ladd | fadd | dadd | ||||
Tsub | isub | lsub | fsub | dsub | ||||
Tmul | imul | lmul | fmul | dmul | ||||
Tdiv | idiv | ldiv | fdiv | ddiv | ||||
Trem | irem | lrem | frem | drem | ||||
Tneg | ineg | lneg | fneg | dneg | ||||
Tshl | ishl | lshl | ||||||
Tshr | ishr | lshr | ||||||
Tushr | iushr | lushr | ||||||
Tand | iand | land | ||||||
Tor | ior | lor | ||||||
Txor | ixor | lxor | ||||||
i2T | i2b | i2s | i2l | i2f | i2d | |||
l2T | l2i | l2f | l2d | |||||
f2T | f2i | f2l | f2d | |||||
d2T | d2i | d2l | d2f | |||||
Tcmp | lcmp | |||||||
Tcmpl | fcmpl | dcmpl | ||||||
Tcmpg | fcmpg | dcmpg | ||||||
if_TcmpOP | if_icmpOP | if_acmpOP | ||||||
Treturn | ireturn | lreturn | freturn | dreturn | areturn |
java虚拟机实际类型和java虚拟机运算类型之间的映射见下表。
一些java虚拟机指令如pop和swap在操作数栈上操作时忽略类型,不过这些指令也必须受到运算类型分类的限制,这些分类也在下表中给出了。
实际类型 | 计算类型 | 分类 |
---|---|---|
boolean |
int |
1 |
byte |
int |
1 |
char |
int |
1 |
short |
int |
1 |
int |
int |
1 |
float |
float |
1 |
reference |
reference |
1 |
returnAddress |
returnAddress |
1 |
long |
long |
2 |
double |
double |
2 |
2.11.2 加载和存储指令
加载和存储指令将值在java虚拟机栈帧的局部变量表和操作数栈之间进行转移。
- 加载一个局部变量到操作数栈: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_m1, iconst_<i>, lconst_<l>, fconst_<f>, dconst_<d>.
- 扩充局部变量表的访问索引的指令:wide
访问对象的属性或者数组的元素也需要和操作数栈进行数据转移。
上面列出的指令助记符中,有一部分是以尖括号结尾的(例如,iload_<n>),这些指令助记符实际上是代表了一组指令(例如iload_<n>有iload_0,iload_1,iload_2,iload3组成)。这几组指令都是某个带有一个操作数的通用指令(如iload)的特殊形式,它们的操作数是隐含的,不需要存储和获取。除此之外,它们的语义和原生通用指令完全一致(如iload_0的语义和操作数为0的iload指令语义完全一致)。尖括号之间的字母指定该指令族的隐式操作数的类型:对于<n>,表示非负整数; 对于<i>,表示int; 对于<l>,表示long; 对于<f>,表示float; 对于<d>,表示double。 在许多情况下,类型int的形式用于对byte,char和short类型的值执行操作。
指令族的概念贯穿这个规范。
2.11.3 运算指令
算数指令将操作数栈上的两个元素进行计算,然后将结果压入操作数栈。主要有两种类型的运算指令,操作整数和操作浮点数。无论哪种类型的运算指令都使用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.
- 取反:ineg, lneg, fneg, dneg.
- 位移:ishl, ishr, iushr, lshl, lshr, lushr.
- 位或:ior, lor.
- 位与:iand, land.
- 位异或:ixor, lxor.
- 局部变量自增:iinc.
- 比较:dcmpg, dcmpl, fcmpg, fcmpl, lcmp.
java虚拟机指令集的语义直接支持java编程语言对于整型和浮点型值操作的语义。
java虚拟机没有明确表示操作整数数值时的溢出情况。整型操作能抛出异常的指令只有整数除指令(idiv,ldiv)和整数求余指令(irem和lrem),当它们除0时,将抛出ArithmeticException
。
2.11.4 类型转换指令
类型转换指令允许java虚拟机中的数字类型相互转换。这些转换操作一般用于实现用户代码的显示类型转换操作,或者用来处理Java虚拟机字节码指令集中指令非完全独立的问题。
java虚拟机直接支持以下数值的宽化类型转换:
- int到long,float或者double
- long到float或者double
- float到double
宽化数值转化指令有i2l,i2f,i2d,l2f,l2d以及f2d。这些操作码的助记符直接从名称上给出了类型转换的内容,使用2来作为双关语表示to。例如,i2d指令表示将int值转换为double值。
注意到宽化数值类型转化不包括将整型类型byte,char,short转化为int。如2.11.1中所述,byte,char和short类型的值在内部扩展为int类型,这使得这些转换是隐式的。
java虚拟机也直接支持下面的窄化数值转化(narrowing numeric conversions):
- int 到 byte,short 或者 char
- long 到 int
- float 到 int 或者 long
- double 到 int,long 或者 float
上面的窄化数值转换是:i2b,i2c,i2s,l2i,f2l,d2i,d2l和d2f。窄化数值转换可能导致结果具有不同的符号,不同的数量级,或者两者都有;转换过程可能导致数值丢失精度。
将int或者long窄化数值转换为整型T,只是简单的丢弃除最低n位之外的所有位,n指类型T的长度。这回导致结果和输入具有不同的符号位。
当将浮点型数值窄化转换为类型T时,T为int或者long,遵循以下规则:
- 如果浮点数为NaN,那么转换结果为int 0或者long 0。
- 否则,如果浮点型数值不是无穷大,那么浮点数将使用IEEE 754的舍入到0模式转换为整数V,并且下面符合下面两种情况:
- 如果T是long类型,并且这个转换的整数值V在long的范围之内,那么结果就是V。
- 如果T是int类型,并且整数值V在int范围之内,那么结果就是V。
- 否则:
- 如果数值非常小(比如一个具有非常大数量级的负数或者负无穷大),那么将用int或者long中的最小的数来表示。
- 如果数值太大(比如一个具有非常大数量级的正数或者正无穷大),那么结果将用int或者long中最大的数来表示。
将double窄化数值转换为float过程和IEEE 754中一致。结果使用IEEE 754的舍入到零模式转换。如果数值太小无法表示为float,那么使用float的正负0来表示;如果数值太大,那么使用float的正负无穷大来表示。double的NaN总是转换为float的NaN。
2.11.5 创建和操作对象
尽管类实例和数组都是对象,但是java虚拟机创建和操作类实例和数组使用不用指令集:
- 创建一个类实例:new
- 创建一个数组:newarray,anewarray,multianewarray。
- 访问类字段(static fields)和类实例的字段(非静态域):getstatic,putstatic,getfield,putfield。
- 加载数组元素到操作数栈:baload,caload,saload,iaload,laload,faload,daload,aaload。
- 从操作数栈中存储一个元素到数组中:bastore,castore,sastore,iastore,lastore,fastore,dastore,aastore
- 获取数组的长度:arraylength
- 检测类实例或者数组的属性:instanceof,checkcast
2.11.6 操作数栈管理指令
提供了一系列指令去直接操作操作数栈:pop, pop2, dup, dup2, dup_x1, dup2_x1, dup_x2, dup2_x2, swap.
2.11.7 控制转移指令
控制转移指令可以让java虚拟机有条件或者无条件的跳转到指定指令处执行,而不是继续执行下一条指令。控制转移指令包括有:
条件分支:ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne.
组合条件分支:tableswitch, lookupswitch.
无条件分支:goto, goto_w, jsr, jsr_w, ret.
2.11.8 方法调用和返回指令
下面五个指令用于方法调用:
invokevitual调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是java语言中最常见的方法分派方式。
invokeinterface调用接口的方法,它会在运行时搜索一个实现了这个接口方法的对象,找出合适的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(2.9),私有方法或者父类的方法。
invokestatic指令用于调用一个类的静态方法。
invokedynamic指令用于调用的方法是绑定到invokedynamic指令的调用端对象的目标。Java虚拟机将调用端对象绑定到invokedynamic指令的特定词法,这是在第一次执行该指令之前运行引导方法的结果。因此,与调用方法的其他指令不同,invokedynamic指令的每次出现都有一个惟一的链接状态。
方法的返回指令通过返回类型来区分,包括:ireturn (用于返回类型是 boolean
, byte
, char
, short
,或者 int
), lreturn, freturn, dreturn, and areturny以及return指令用于返回值是void的方法,实例初始化方法,类或者接口初始化方法的返回。
2.11.9 抛出异常
在程序中抛出异常使用athrow指令。多个java虚拟机指令在检测到异常情况是也会抛出异常。
2.11.10 同步
java虚拟机支持方法同步和方法中一系列指令同步,同步使用同步结构:monitor
方法级别的同步是隐式的,作为方法调用和返回的一部分。方法调用指令检查运行时常量池中的method_info结构中的ACC_SYNCHRONIZED标识来区分一个方法是不是同步方法。当调用一个设置了ACC_SYNCHRONIZED标识的方法,执行线程进入monitor,调用这个方法,当方法不管是正常或者异常调用结束时,线程退出monitor。在执行这个方法的时间段内,只有执行的线程拥有这个monitor,没有其他线程可以进入。当调用同步方法时抛出异常,并且在同步方法内部没有处理这个异常,那么这个方法的monitor将在同步方法向外抛出异常前自动退出。
指令序列的同步通常使用java编程语言中的synchronized块。java虚拟机提供monitorenter和monitorexit指令来支持这个语法。正确实现synchronized需要编译器和java虚拟机合作完成。
结构化锁定(Structured locking)是指在方法调用期间每一个给定的monitor退出都将匹配这个这个monitor之前的进入的情形。由于没法保证所有提交给java虚拟机的代码都会满足结构化锁定,java虚拟机的实现者允许(不是必需的)强制执行下面两条规则来保证结构化锁定。其中T表示一个线程,M表示一个monitor。
1、当方法调用完成时,不管时正常结束还是异常退出,T进入M的次数必需和T离开M的次数相等。
2、在方法调用过程中,任何时刻都不会出现T离开M的次数多余T进入M的次数。
注意,当调用一个同步方法时,java虚拟机自动执行monitor的进入和退出也被认为时在方法调用期间完成。
2.12 类库
java虚拟机对于java se平台实现类库必须提供足够的支持。这些类库中的一些类如果没有java虚拟机的协作将无法实现。
支持以下功能的类可能需要java虚拟机的特殊支持:
- 反射,例如java.lang.reflect中的类和Class类。
- 加载和创建一个类或者接口。上面提到的列子也适用于这点
- 链接和初始化一个类或者接口。上面提到的列子也适用于这点
- 安全,例如java.security包中的类和其它例如SecurityManager的类
- 多线程,例如Thread类
- 弱引用,例如java.lang.ref包中的类。
上面的列表是为了说明问题,而不是全面的介绍类库。这些类或它们提供的功能的详尽列表超出了本规范的范围。有关详细信息,请参阅Java SE平台类库的规范。