实战java虚拟机10-虚拟机指令

实战java虚拟机
深入理解java虚拟机

使用javap查看Class信息

字节码执行之于java虚拟机,如同汇编语言之于计算机一样重要。字节码是java虚拟机来说是执行的根本。当java源码被编译成class文件之后,虚拟机就会将class文件内的方法字节码载入系统并加以执行。
每一个java字节码指令是一个byte数字,并且有一个对应的住记符。目前所有的字节码指令大约有200个:
这里写图片描述
一个方法的java字节码指令,被编译到java方法的Code属性中,如果想要查看指令的具体内容,可用JDK自带javap工具。

  -version                 版本信息
  -v  -verbose             附加信息
  -l                       Print line number and local variable tables
  -public                  Show only public classes and members
  -protected               Show protected/public classes and members
  -package                 Show package/protected/public classes
                           and members (default)
  -p  -private             Show all classes and members
  -c                       对代码进行反汇编
  -s                       Print internal type signatures
  -sysinfo                 Show system info (path, size, date, MD5 hash)
                           of class being processed
  -constants               Show static final constants
  -classpath <path>        Specify where to find user class files
  -bootclasspath <path>    Override location of bootstrap class files

以下面这一段简单代码为例:

package cn.jhs.chap11;

public class Calc {
    public int calc() {
        int a = 500;
        int b = 200;
        int c = 50;
        return (a+b)/c;
    }
}

使用$ javap -v cn/jhs/chap11/Calc查看:

##显示源文件名称#####
Classfile /E:/workspaces/idea/vm/vm_in_action/chap08/src/cn/jhs/chap11/Calc.class
  Last modified 2018-7-14; size 276 bytes
  MD5 checksum 349610279551c2967434c23c5ad574b5
  Compiled from "Calc.java"
public class cn.jhs.chap11.Calc
  SourceFile: "Calc.java"
##版本信息##
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER

##常量池信息###  
Constant pool:
   #1 = Methodref          #3.#12         //  java/lang/Object."<init>":()V
   #2 = Class              #13            //  cn/jhs/chap11/Calc
   #3 = Class              #14            //  java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               calc
   #9 = Utf8               ()I
  #10 = Utf8               SourceFile
  #11 = Utf8               Calc.java
  #12 = NameAndType        #4:#5          //  "<init>":()V
  #13 = Utf8               cn/jhs/chap11/Calc
  #14 = Utf8               java/lang/Object
{
###编译器自动插入,类的无参的构造函数
  public cn.jhs.chap11.Calc();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 8: 0

##calc方法##
  public int calc();
    flags: ACC_PUBLIC
    Code:
##Stack表示操作数栈深度。操作数栈中最多有多少个参数。
##局部变量表大小:非static方法,第一个(index从0开始)局部变量都是this;
##入参个数(this)
          stack=2, locals=4, args_size=1
         0: sipush        500  ##自然偏移量0,将给定参数500压入操作数栈,sipush占1字节,int占2字节,故下一个字节码的偏移量为3.sipush处理范围 -32768~32767
         3: istore_1           ##自然偏移量3,将操作数栈弹出一个元素,并存放在局部变量表第1个位置
         4: sipush        200
         7: istore_2
         8: bipush        50  ##bipuch处理范围 -128~127
        10: istore_3
        11: iload_1           ##将局部变量表第一个变量 压入操作数栈
        12: iload_2
        13: iadd              ##将操作数栈中两个数做加法运算
        14: iload_3
        15: idiv              ##将操作栈中两个数做除法运算
        16: ireturn           ##将函数操作数栈顶层元素弹出,并压入调用者函数的操作数栈中。如果当前方法是synchronized,还隐含了一个monitorexit指令退出临界区。最终弹出当前函数的整个栈帧。

##行号信息##
      LineNumberTable:
        line 10: 0   ###代码第10行 与自然偏移量0有关。
        line 11: 4
        line 12: 8
        line 13: 11
}

虚拟机规范中并没有明确的指定Slot应占用的内存大小,只是很有导向性的说到每个Slot都应该能存放一个
boolean,byte,char,short,int,float,reference或returnAddress类型数据。



java虚拟机常用指令

常量入栈指令
常量入栈指令是指将常熟压入操作数栈,根据数据类型和入栈内容的不同又可以分为const,push和ldc系列。

  • const:用于特定的常量入栈,入栈的常量隐含在命令中。
    • 比如aconst_null,将null压入操作数栈,iconst_m1将-1压入操作数栈,fconst_2将浮点数2压入栈。(iconst_x,x为0~5)
    • i:表示整数,l表示长整型,f表示浮点数,d表示双精度浮点数,a表示对象引用。
  • push:主要包含bipush和sipush.它们的不同是bipush接收8位整数作为参数,sipush接收16位整数作为参数。bipush 20 ; sipush 500;
  • ldc:如果以上指令都无法满足需求,则使用万能指令ldc.它可以接收一个8位的参数,该参数指向常量池中int,float或者string的索引,并将指定内容压入操作数栈。例如: ldc #27
    • ldc 把常量池中的项压入栈
    • ldc_w 把常量池中的项压入栈(使用宽索引)
    • ldc2_w 把常量池中long类型或者double类型的项压入栈(使用宽索引)

局部变量表压栈
局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。以load形式存在,这类指令包含:

  • xload_n(x为x,l,f,d,a,n为0到3):将第n个局部变量压入操作数栈。例如:iload_1,aload_2
  • xload(x为i,l,f,d,a):使用此命令,此时局部变量超过4个时。比如:aload 5;
  • xaload(x为x,l,f,d,a,b,c,s):表示将数组的元素压栈。xaload执行前,现将数组引用入栈(aload_1),再将数组索引(iconst_3)入栈
public void print3(short[] s){
    System.out.println(s[3]);
}

编译后字节码指令为:

0: getstatic     #2       // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_1          #####局部变量表1, 即数组 a 入栈
4: iconst_3         #####将数组索引 i 入栈
5: saload           #####saload 将short数组元素压栈。
6: invokevirtual #3       // Method java/io/PrintStream.println:(I)V
9: return

这里写图片描述


出栈装入局部变量表
出栈装入局部变量表表示指令将操作数栈中的栈顶元素弹出,装入局部变量的指定位置,用于给局部变量赋值。这类指令主要以store形式存在。类似于load:
- xstore_n(x为x,l,f,d,a,n为0到3):istore_1 将栈顶元素,赋值给局部变量1
- xstore(x为i,l,f,d,a):由于没有隐含参数信息,故需要提供一个byte类型的参数来指定目标局部变量表的位置。
- xastore(x为x,l,f,d,a,b,c,s):如iastore它用于给一个int数组指定索引赋值。在执行iastore执行前,操作数栈顶需要准备3个元素:值,索引,数组引用

java代码:

    public void print4(int[] s){
        int i,j,k;
        i = 21;
        s[3]=77;
    }

编译后字节码指令为:

0: bipush        21    ###常量21 入栈
2: istore_2            ###21 赋值给局部变量2
3: aload_1             ###加载局部变量1,即数组 a
4: iconst_3            ###常量3入栈,索引 i,
5: bipush        77    ###常量77入栈, 值 value
7: iastore             ###数组赋值
8: return

通用型操作
从上述的几类指令中可以看到,大部分的数据操作指令时和数据类型相关的。java虚拟机为不同的数据类型都量身定做了一些列的指令。但是无类型的指令还是必要的,通用型操作就提供了这种无需指明数据类型的操作。

  • NOP:是一个非常特殊的指令,它的字节码是0x00,和汇编的nop一样,它表示什么也不做。这条指令一般可用于调试、占位等
  • dup:duplicate复制,将栈顶元素复制一份并在此压入栈顶,这样栈顶就有2个一模一样的元素。dup2
  • pop:把一个元素从栈顶弹出,并且直接废弃。pop一个字长(32位),要丢弃64位数据,可使用pop2

java源码示例:

public void print4(int i){
    Object obj = new Object();
    obj.toString();
}

编译后字节码指令为:

###使用创建对象指令new,创建文成后,将对象引用放置在栈顶
0: new           #4   // class java/lang/Object  
3: dup              ###dup复制一份对象的引用
##执行调用对象的构造函数,此时需要将对象引用,故会将栈顶元素弹出一次
4: invokespecial #1   // Method java/lang/Object."<init>":()V
7: astore_2         ###将栈顶元素 复制给局部变量表2
8: aload_2          ###局部变量表2 ,入栈
9: invokevirtual #5   // Method java/lang/Object.toString:()Ljava/lang/String;
12: pop
13: return

类型转换指令
在软件开发的过程中,数据类型转换是很常见的功能。为了很好的支持类型转换,虚拟机提供了一整套专门用于类型转换的指令。这类命令基本上使用x2y的形式给出。i2l,将i转换为long数据。指令i2l执行时,首先将int数据入栈,然后进行转换,然后进行赋值(出栈)。

public void print5(int i) {
        long l = i;
        int j = (int) l;
    }
}

编译后字节码指令为:

0: iload_1
1: i2l
2: lstore_2
3: lload_2
4: l2i
####这里需要注意的地方,为什么不是istore_3,而是istore_4。是因为long是64位数据类型,会在局部变量表中占用两个位置。
5: istore        4  
7: return

这里写图片描述
实际上指令中有i2b,i2c,i2s,但是并没有b2i,c2i,s2i.
- 对于byte转换为int,虚拟机并没有做实质化的处理。
- 对于byte转long,使用的指令时i2l,在虚拟机内部,此时已经将byte等同于int处理。

为什么会这样处理呢?
一方面可以减少指令数量。 因为目前虚拟机只有用一个字节来表示指令,最多只能支持256个指令,为了节省指令资源。
另一方面,无论是byte,short在局部变量表中都会占用32位空间,没有必要特意区分集中数据类型。


运算指令

  • 加法:xadd(i,l,f,d,代表int,long,float,double);
  • 减法:xsub
  • 乘法:xmul
  • 除法:xdiv
  • 取余:xrem
  • 取相反数:xneg
  • 自增指令:iinc。iinc index,value ; index表示在局部变量表位置,value表示自增的值。
  • 位运算
    • 位移指令:ishl,ishr;lshl,lshr; iushr,lushr 将int,long右移动(无符号)
    • 或:ior,lor
    • 与:iand,land
    • 异或:ixor

对象/数组操作指令
(1).创建对象
- new :创建普通对象。
- newarray:创建基本类型数组。newarray int ,newarray long..
- anewarray:创建对象数组。anewarray #3
- multianewarray :创建多维数组。

public void print8() {
        long[] a1 = new long[10];
        String[] a2 = new String[3];
        int[][] a3 = new int[5][6];
    }

编译后字节码指令为:

0: bipush        10
2: newarray       long
4: astore_1
5: iconst_3
6: anewarray     #10                 // class java/lang/String
9: astore_2
10: iconst_5
11: bipush        6
13: multianewarray #11,  2            // class "[[I"   ####2表示2纬数组。
17: astore_3
18: return

(2).字段访问指令

  • getfield:实例对象字段。实例对象字段访问,会比类静态字段访问多一个值操作数。即实例变量自身。
  • putfield
  • getstatic: 类静态字段。
  • putstatic

实例代码:

class User{
    public String name = "aa";
    public static int type = 3;

}

public void print9(User user){
    String tempName = user.name;
    user.name = "bb";

    int tempType = User.type;
    User.type = 33;
}

编译后字节码指令为:

 0: aload_1
 1: getfield      #13        // Field cn/jhs/chap11/User.name:Ljava/lang/String;
 4: astore_2              ####将局部变量1的,#13字段值,保存至局部变量2.
 5: aload_1
 6: ldc           #14        // String bb
 8: putfield      #13        // Field cn/jhs/chap11/User.name:Ljava/lang/String;
11: getstatic     #15        // Field cn/jhs/chap11/User.type:I
14: istore_3
15: bipush        33
17: putstatic     #15        // Field cn/jhs/chap11/User.type:I
20: return

(3).类型检查指令

  • checkcast:强制转换是否可以进行,如果可以指令不会改变操作数栈,如果不可行抛出ClassCastException.
  • instanceof:判断给定对象是否是某一个类的示例,它会将判断结果压入操作数栈。

代码示例:

    public String checkcast(Object obj) {
        if(obj instanceof String){
            return (String) obj;
        }
        return null;
    }

编译后字节码指令为:

0: aload_1
1: instanceof    #10                 // class java/lang/String
4: ifeq          12
7: aload_1
8: checkcast     #10                 // class java/lang/String
11: areturn
12: aconst_null
13: areturn

(4).数组操作指令

  • xastore
  • xaload
  • arraylenth: 该指令弹出栈顶的数组元素,获取数组的长度,将长度压入栈。

比较控制指令
(1).比较指令
比较指令的作用是比较栈顶两个元素的大小,并将结果入栈。 dcmpg,dcmpl ; fcmpg,fcmpl ; lcmp
首字符d表示double,f表示float,l表示long.对于数字double,float,由于有NaN的存在,故各有两个版本。

栈顶为v1,栈顶顺位第2元素是v2, 若v1=v2,则压入0;若v1>v2 ,则压入1
;若v1<v2则压入-1;若遇到NaN值,fcmpg会压入1,fcmpl则压入-1;

(2).条件跳转指令
条件跳转指令通常和比较指令结合使用。条件跳转指令有:ifeq,iflt,ifle,ifne,ifgt,ifge,ifnull,ifnonnull这些指令都接收两个字节的操作数,用于计算跳转的位置(16位符号整数作为当前位置的offset).它们的统一含义是:弹出栈顶元素,测试它是否满足某一条件,如果满足则跳转到指定位置。在条件跳转执行前,一般可以先用比较指令进行栈顶元素的准备,然后再进行条件跳转。
这里写图片描述
代码示例:

    public void cmp1() {
        float f1 = 9;
        float f2 = 10;
        System.out.println(f1 > f2);
    }

编译后字节码指令为:

0: ldc           #16                 // float 9.0f
2: fstore_1
3: ldc           #17                 // float 10.0f
5: fstore_2
6: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
9: fload_1
10: fload_2
11: fcmpl               #### 比较 fload_1 < fload_2,并将结果压入栈顶
12: ifle          19    ####获取栈顶比较结果,如果 <= 则跳转到19行,否则书序执行
15: iconst_1            
16: goto          20
19: iconst_0
20: invokevirtual #18                 // Method java/io/PrintStream.println:(Z)V
23: return

(3).比较条件跳转指令
比较条件跳转指令类似于比较指令和条件跳转指令的结合体。类似指令有:if_icmpeq,if_icmpne,if_icmpgt,if_icmplt,if_icmple,if_icmpge和if_acmpeq,if_acmpne,其中助记符加上“if_”后,以字符“i”开头的指令指针对int整数操作(包含short和byte类型),以字符“a”开头的指令表示对象引用比较。运行此指令时,栈顶需要准备两个元素进行比较。
重点内容

9: iload_1
10: iload_2
11: if_icmple     18

(4).多条件分支跳转
多条件分支跳转指令时转为switch-case语句设计的,主要有tableswitch和lookupswitch。

  • tableswitch:条件分支值是连续的,它内部只存放起始值核终止值,以及若干个跳转偏移量,通过给定的操作数index,可以立即定位到跳转偏移量上,因此效率较高。
  • l**ookupswitch**:内部存放着各个离散的case-offset对,每次执行都要搜索全部的case-offset对,找到匹配的case值,并根据对应的offset计算跳转地址,因此效率较低。

(5).无条件跳转
目前主要的无条件跳转指令为goto.指令jsr,ret虽然也是无条件跳转的,但主要用于try-finally语句,且已经被虚拟机逐渐废弃。如果指令偏移量太大,则它可以使用goto_w,它接收4个字节作为操作数。


函数调用与返回指令

  • invokevirtual:虚函数调用,调用对象的实例方法,根据对象的实际类型进行转发,支持多态,是目前最常用的java函数调用方式。
  • invokeinterface:指接口方法的调用,当被调用对象申明为接口时,使用该指令。
  • invokespecial:调用特殊方法,比如构造方法,类的私有方法,父类方法。这些方法都是静态类型绑定的,不会在调用时进行动态绑定。
  • invokestatic:调用类静态方法。
  • invokedynamic:调用动态绑定的方法,JDK1.7后新加入的指令。

函数调用结束前,需要进行返回。返回时,需要使用xreturn指令将返回值存入调用者的操作数栈中。当返回int时,ireturn;当返回值是void时,使用指令return,返回值为引用类型时,使用areturn.
示例代码:

    public String rtn(){
        String msg = "hello world";
        System.out.println(msg);
        return msg;
    }

编译后字节码指令为:

0: ldc           #19     // String hello world
2: astore_1
3: getstatic     #2      // Field java/lang/System.out:Ljava/io/PrintStream;
6: aload_1
###调用PrintStream.void()方法
7: invokevirtual #20     // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: aload_1
11: areturn      ##### areturn

同步控制
为了实现多线程同步,java虚拟机提供了monitorenter和monitorexit两条指令来完成临界区的进入和离开操作。

> 当一个线程进入同步块时,它使用monitorenter指令请求进入,如果当前对象的监视器计数器为0,则它会被准许进入,若为1,则判断当前监视器的线程是否为自己,如果是,则进入,否则进行等待,知道对象的监视器计数为0,它才会被允许进入同步块。当线程退出同步块时,需要使用monitorexit申明退出。

实例代码:

private int i = 0;
public synchronized void add1(){
    i++;
}

public void add2(){
    synchronized (this) {
        i++;
    }
}

编译后字节码指令为:

//add1():没有使用monitorenter和monitorexit进行同步区控制,但是当虚拟机通过方法标识符判断是一个同步方法时,会自动在方法调用前进行加锁。
0: aload_0
1: dup
2: getfield      #2                  // Field i:I
5: iconst_1
6: iadd
7: putfield      #2                  // Field i:I
10: return


//add2():有两个monitorexit,第二个monitorexit结合着athrow指令使用。虽然在源码中没有进行异常处理,但是在字节码中会自动插入
0: aload_0
1: dup
2: astore_1
3: monitorenter   ###monitorenter
4: aload_0
5: dup
6: getfield      #2                  // Field i:I
9: iconst_1
10: iadd
11: putfield      #2                  // Field i:I
14: aload_1
15: monitorexit  ###第一个monitorexit
16: goto          24  ###正常,则跳转24,直接结束方法。
19: astore_2     ####异常处理节点....
20: aload_1
21: monitorexit  ####第二个monitorexit
22: aload_2      ####异常信息,压入栈顶
23: athrow       
24: return
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值