类文件结构---字节码指令简介

一个简单的例子:

public class Hello{

public static void main(String[] args){
int i=0;
i=i++;
System.out.println(i);

}

}

这段代码会输出多少?不妨进行反编译一下。



通过javap大致可以将上面的两行代码翻译成如下的JVM指令执行代码。
0: iconst_0
1: istore_1
2: iload_1
3: iinc          1, 1
6: istore_1
10: iload_1

接下来分析一下JVM是如何执行的:
第0:将int类型的0入栈,就是放到操作数栈的栈顶
第1:将操作数栈栈顶的值0弹出,保存到局部变量表 index (索引)值为1的位置。(局部变量表也是从0开始的,0位置一般保存当前实例的this引用,当然静态方法例外,因为静态方法是类方法而不是实例方法)
第2:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)
第3:iinc是对int类型的值进行自增操作,后面第一个数值1表示,局部变量表的index值,说明要对此值执行iinc操作,第二个数值1表示要增加的数值。(这时局部变量表index为1的值因为执行了自增操作变为1了,但是操作数栈中栈顶的值仍然是0)
第6:将操作数栈顶的值弹出(值0),放到局部变量表index为1的位置(旧值:1,新值:0),覆盖了上一步局部变量表的计算结果。
第10:将局部变量表index 1位置的值的副本入栈。(这时局部变量表index为1的值是0,操作数栈顶的值也是0)

总结:从执行顺序可以看到,这里第1和第6执行了2次将0赋值给变量i的操作(=号赋值),i++操作是在这两次操作之间执行的,自增操作是对局部变量表中的值进行自增,而栈顶的值没有发生变化,这里需要注意的是保存这个初始值的地方是操作数栈而不是局部变量表,最后再将栈顶的值覆盖到局部变量表i所在的索引位置中去。

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

java虚拟机指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码。

 

字节码与数据类型

由于操作码长度只有一个字节,如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令的数量会远远超出一个字节所能表示的数量范围,因此,指令集对于特定的操作只提供了有限的类型去支持他(“Not Orthogonal”),有一些单独的指令可以在必要的时候用来将支持一些不支持的类型转换为可被支持的类型。

大部分的指令都没有支持整数类型byte、char、short,甚至没有任何指令支持boolean类型,编译器会在编译期或者运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型数据零位扩展为乡音的int类型数据。

2 加载和存储指令

  1. 将一个局部变量加载到操作栈的指令包括有: 
    iload、iload_<n>、lload、lload_<n>、fload、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_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>

  4. 扩充局部变量表的访问索引的指令:wide

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、lneg、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 
      

4 类型转换指令

Java虚拟机对于宽化类型转换直接支持,并不需要指令执行,包括:

  1. int类型到long、float或者double类型
  2. long类型到float、double类型
  3. float类型到double类型

窄化类型转换指令包括有 
i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f

窄化类型转换很可能会造成精度丢失。

对象创建与访问指令

虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令(在第7章会讲到数组和普通类的类型创建过程是不同的)。对象创建后,就可以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素,这些指令如下。

  • 创建类实例的指令:new。
  • 创建数组的指令 : newarray、anewarray、multianewarray。
  • 访问类字段(static字段 ,或者称为类变量)和实例字段(非static字段 ,或者称为实例变量 )的指令:getfield、putfield、getstatic、putstatic。
  • 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faloads daload、aaload。
  • 将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
  • 检查类实例类型的指令:instanceof、checkcast。

操作数栈管理指令

如同操作一个普通数据结构中的堆栈那样,Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:

  • 将操作数栈的栈顶一个或两个元素出栈:pop、pop2。
  • 复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、 dup_x1、dup2_x1、dup_x2、dup2_x2。
  • 将栈最顶端的两个数值互换 : swap。

6 控制转移指令:

  1. 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
  2. 复合条件分支:tableswitch、lookupswitch
  3. 无条件分支:goto、goto_w、jsr、jsr_w、ret 控制转移指令:

7 方法调用和返回指令

  1. invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
  2. invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
  3. invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
  4. invokestatic指令用于调用类方法(static方法)
  5. invokedynamic指令用于调用以绑定了invokedynamic指令的调用点对象(call site object)作为目标的方法。调用点对象是一个特殊的语法结构,当一条invokedynamic指令首次被Java虚拟机执行前,Java虚拟机将会执行一个引导方法(bootstrap method)并以这个方法的运行结果作为调用点对象。因此,每条invokedynamic指令都有独一无二的链接状态,这是它与其他方法调用指令的一个差异。
  6. 方法返回指令则是根据返回值的类型区分的,包括有

    1. ireturn(当返回值是boolean、byte、char、short和int类型时使用)
    2. lreturn(long)、freturn(float)、dreturn(double)和areturn(reference)
    3. return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用

8 抛出异常指令

athrow

同步指令

Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。

方法级的同步是隐式的,即无须通过字节码指令来控制,它实现在方法调用和返回操作之中。虛拟机可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程就要求先成功持有管程,然后才能执行方法, 最后当方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获取到同一个管程。如果一个同步方法执行期间拋出了异常,并且在方法内部无法处理此异常,那么这个同步方法所持有的管程将在异常拋到同步方法之外时自动释放。

同步一段指令集序列通常是由Java语言中的synchronized语句块来表不的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要Javac编译器与Java虚拟机两者共同协作支持,譬如代码清单6-6中所 示的代码。

posted @ 2017-07-28 11:10  竹马今安在  阅读(322)  评论(0编辑  收藏  举报