读懂Java字节码(2)- 字节码基础

从栈帧说起

读懂Java字节码(2)- 字节码基础_第1张图片
方法-栈帧的组成

之前说道每个方法的栈帧包括了局部变量表,操作数栈,方法出口等信息。

局部变量表

前面提到java执行每个方法的时候会有一个栈帧,栈帧有包括了局部变量表和操作数栈(operand stack)。
其中局部变量表记录了所有方法运行过程中用到的变量,包括:

  • this变量
  • 方法参数
  • 局部变量
    对于类方法(也就是static方法)来说,方法参数是从第0个位置开始的,而对于实例方法来说,第0个位置上的变量是this指针。
    局部变量可以包括如下的属性:
  • char
  • byte
  • boolean
  • int
  • short
  • long
  • float
  • double
  • 引用
  • 返回地址
    在32位虚拟机上除了long和double类型外,每个变量都只占局部变量区中的一个变量槽(slot),而long及double会占用两个连续的变量槽,因为这些类型是64位的。

常见的变量赋值

当一个新的变量创建的时候,操作数栈会用来存储这个新变量的值。然后这个变量会存储到局部变量区中对应的位置上。如果这个变量不是基础类型的话,本地变量槽上存的就只是一个引用。这个引用指向堆的里一个对象。
比如int i = 5; 字节码对应了:

0: iconst_5
2: istore_0

每一个指令的解释如下:

操作指令 解释
iconst_5 用来将一个字节作为整型数字压入操作数栈中,在这里5就会被压入操作数栈上。
istore_0 这是istore_这组指令集(译注:严格来说,这个应该叫做操作码,opcode ,指令是指操作码加上对应的操作数,oprand。不过操作码一般作为指令的助记符,这里统称为指令)中的一条,这组指令是将一个整型数字存储到本地变量中。n代表的是局部变量区中的位置,并且只能是0,1,2,3。再多的话只能用另一条指令istore了,这条指令会接受一个操作数,对应的是局部变量区中的位置信息。

但是如果换成如下语句 int i = 6; 则对应了

0: bipush 6
2: istore_0

造成这种原因的差别源于字节码指令集的定义:iconst指令(将常量加载到操作栈)只包含了0-5,大于5则会使用bipush。

同时还有一点需要注意的是,在class文件中,被初始化的变量操作都会加到构造方法里面。

  public com.ys.testmodel.Loo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: bipush        6
         7: putfield      #2                  // Field a:I
        10: return
}

相关指令的解释如下:

操作指令 解释
aload_0 从局部变量数组中加载一个对象引用到操作数栈的栈顶。尽管这段代码看起来没有构造方法,但是在编译器生成的默认的构造方法里,就会包含这段初始化的代码。第一个局部变量正好是this引用,于是aload_0把this引用压到操作数栈中。aload_0是aload_指令集中的一条,这组指令会将引用加载到操作数栈中。n对应的是局部变量数组中的位置,并且也只能是0,1,2,3。还有类似的加载指令,它们加载的并不是对象引用,比如iload_,lload_,fload_,和dload_, 这里i代表int,l代表long,f代表float,d代表double。局部变量的在数组中的位置大于3的,得通过iload,lload,fload,dload,和aload进行加载,这些指令都接受一个操作数,它代表的是要加载的局部变量的在数组中的位置。
invokespecial 这条指令可以用来调用对象实例的构造方法,私有方法和父类中的方法。它是方法调用指令集中的一条,其它的还有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.这里的invokespecial指令调用的是父类也就是java.lang.Object的构造方法。
bipush 它是用来把一个字节作为整型压到操作数栈中的,在这里6会被压到操作数栈里。
putfield 它接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,在这里这个字段是simpleField。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都 会从操作数栈顶上pop出来。前面的aload_0指令已经把包含这个字段的对象压到操作数栈上了,而后面的bipush又把6压到栈里。最后putfield指令会将这两个值从栈顶弹出。执行完的结果就是这个对象的simpleField这个字段的值更新成了6。

这里的putfield指令的操作数引用的是常量池里的第二个位置。JVM会为每个类型维护一个常量池,运行时的数据结构有点类似一个符号表,尽管它包含的信息更多。Java中的字节码操作需要对应的数据,但通常这些数据都太大了,存储在字节码里不适合,它们会被存储在常量池里面,而字节码包含一个常量池里的引用 。当类文件生成的时候,其中的一块就是常量池:

Constant pool:
   #1 = Methodref          #4.#10         // java/lang/Object."":()V
   #2 = Fieldref           #3.#11         // com/ys/testmodel/Loo.a:I
   #3 = Class              #12            // com/ys/testmodel/Loo
   #4 = Class              #13            // java/lang/Object
   #5 = Utf8               a
   #6 = Utf8               I
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = NameAndType        #7:#8          // "":()V
  #11 = NameAndType        #5:#6          // a:I
  #12 = Utf8               com/ys/testmodel/Loo
  #13 = Utf8               java/lang/Object

Flag

上面的变量flas是空的,因为代码中没有增加修饰符

  int a;
    descriptor: I
    flags:

final修饰符
如果写成如下形式

    public final int a = 6;

则对应的字节码为

  public final int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_FINAL
    ConstantValue: int 6

static修饰符

public static int a = 6;

对应的字节码为

  public static int a;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

同时初始化代码并不在构造方法里面,而是转移到静态代码块

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: bipush        6
         2: putstatic     #2                  // Field a:I
         5: return

字节码指令集合

下面简单罗列了字节码指令集
加载和存储指令:

将一个局部变量加载到操作栈的指令包括有: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_m1、iconst_、lconst_、fconst_、dconst_

运算指令

加法指令: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虚拟机对于宽化类型转换直接支持,并不需要指令执行,包括:
int类型到long、float或者double类型
long类型到float、double类型
float类型到double类型
窄化类型转换指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。但是窄化类型转换很可能会造成精度丢失

对象创建与操作指令:

创建类实例的指令:new
创建数组的指令:newarray,anewarray,multianewarray

访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者成为实例变量)的指令:

getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore

取数组长度的指令:arraylength

检查类实例类型的指令:
instanceof、checkcast

操作数栈管理指令:

Java虚拟机提供了一些用于直接操作操作数栈的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、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指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法(§2.9)、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。

而方法返回指令则是根据返回值的类型区分的,包括有ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法、类和接口的类初始化方法使用

抛出异常指令:
athrow

你可能感兴趣的:(读懂Java字节码(2)- 字节码基础)