专栏首页算法之名Java字节码 顶

Java字节码 顶

Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。每一个Java字节码指令是一个byte数字,并且有一个对应的助记符。

Java虚拟机常用指令

常量入栈指令

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

const系列

aconst_null 将null压入操作数栈

iconst_m1 将-1压入操作数栈

iconst_x 将x压入栈

lconst_0 将长整数0压入栈

lconst_1 将长整数1压入栈

fconst_0 将浮点数0压入栈

dconst_0 将double型0压入栈

其中i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点,a表示对象引用,一般用来压入数组的索引。

push系列

bipush 接收8位整数作为参数

sipush 接收16位整数作为参数

ldc指令:

ldc 它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将制定的内容压入堆栈。

ldc_w 它接收两个8位参数,能支持的索引范围大于ldc.

ldc2_w 压入的元素为long或者double类型

假设我们写一个这样的类

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

编译后,期编译的Calc.class文件位于/Users/admin/Downloads/calculate/target/classes/com/guanjian/calculate/test下面,进入该文件夹,执行命令

javap -v Calc

获得如下内容

警告: 二进制文件Calc包含com.guanjian.calculate.test.Calc
Classfile /Users/admin/Downloads/calculate/target/classes/com/guanjian/calculate/test/Calc.class
  Last modified 2020-5-25; size 457 bytes
  MD5 checksum 7088989dfee01d7aa7dcde0b23f91621
  Compiled from "Calc.java"
public class com.guanjian.calculate.test.Calc
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
   #2 = Integer            500000
   #3 = Long               50l
   #5 = Class              #24            // com/guanjian/calculate/test/Calc
   #6 = Class              #25            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/guanjian/calculate/test/Calc;
  #14 = Utf8               calc
  #15 = Utf8               ()D
  #16 = Utf8               a
  #17 = Utf8               I
  #18 = Utf8               b
  #19 = Utf8               c
  #20 = Utf8               J
  #21 = Utf8               SourceFile
  #22 = Utf8               Calc.java
  #23 = NameAndType        #7:#8          // "<init>":()V
  #24 = Utf8               com/guanjian/calculate/test/Calc
  #25 = Utf8               java/lang/Object
{
  public com.guanjian.calculate.test.Calc();
    descriptor: ()V
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/guanjian/calculate/test/Calc;

  public double calc();
    descriptor: ()D
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=5, args_size=1
         0: ldc           #2                  // int 500000
         2: istore_1
         3: sipush        200
         6: istore_2
         7: ldc2_w        #3                  // long 50l
        10: lstore_3
        11: iload_1
        12: iload_2
        13: iadd
        14: i2l
        15: lload_3
        16: ldiv
        17: l2d
        18: dreturn
      LineNumberTable:
        line 5: 0
        line 6: 3
        line 7: 7
        line 8: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      19     0  this   Lcom/guanjian/calculate/test/Calc;
            3      16     1     a   I
            7      12     2     b   I
           11       8     3     c   J
}
SourceFile: "Calc.java"

其中Constant pool为常量池。其中有这么几行字节码指令

0: ldc           #2                  //常量池中#2为500000,将500000压入栈
2: istore_1
3: sipush        200。               //200转成二进制是11001000,大于8位小于16位,按16位压栈符压栈
6: istore_2
7: ldc2_w        #3                  //常量池中#3为长整数50l,故使用ldc2_w压入栈

局部变量压栈指令

局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。

xload 通过指定参数的形式,将局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个。

xload_n 表示将第n个局部变量压入操作数栈,比如iload_1、fload_0、aload_0,aload_n表示将一个对象引用压栈。(n为0到3)

xaload 表示将数组的元素压栈,比如saload、caload分别表示压入sort数组和char数组。指令xaload在执行时,要求操作数中栈顶元素为数组索引i,栈顶顺位第2个元素为数组a,该指令会弹出栈顶这两个元素,并将a[i]重新压入堆栈。

x取值

含义

i

int整数

l

长整数

f

浮点数

d

双精度浮点

a

对象索引

b

byte

c

char

s

short

现在我们来看java的这样一个方法

public void print(char[] cs,short[] s) {
    System.out.println(s[0]);
    System.out.println(cs[0]);
}

编译后的字节码为

public void print(char[], short[]);
    descriptor: ([C[S)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=3
         0: getstatic     #5                  
         3: aload_2                           //将数组引用s入栈,由于s是局部变量表第二个参数,故而尾数为2
         4: iconst_0                          //将索引0入栈
         5: saload                            //弹出栈顶的两个元素(第一个是数组索引0,第二个是数组引用s),把short数组元素s[0]重新入栈
         6: invokevirtual #6                  
         9: getstatic     #5                  
        12: aload_1                           //将数组引用cs入栈,由于cs是局部变量表第一个参数,故而尾数为1
        13: iconst_0                          //将索引0入栈
        14: caload                            //弹出栈顶的两个元素(第一个是数组索引0,第二个是数组引用cs),把char数组元素cs[0]重新入栈
        15: invokevirtual #7                  
        18: return
      LineNumberTable:
        line 12: 0
        line 13: 9
        line 14: 18
      LocalVariableTable:                     //局部变量表
        Start  Length  Slot  Name   Signature
            0      19     0  this   Lcom/guanjian/calculate/test/Calc;
            0      19     1    cs   [C        //局部变量表第一个参数
            0      19     2     s   [S。      //局部变量表第二个参数
    MethodParameters:
      Name                           Flags
      cs
      s

出栈装入局部变量表指令

出栈装入局部变量表指令用于将操作数栈中栈顶元素弹出后,装入局部变量表的制定位置,用于给局部变量赋值。

xstore 通过指定参数的形式,将操作数栈中弹出一个值(x为i、l、f、d、a),当使用这个命令时,表示局部变量的数量可能超过了4个

xstore_n 将操作数栈中弹出一个值赋值给第n个局部变量(x为i、l、f、d、a)(n为0到3)

xastore 专门针对数组操作,用于给一个数组的给定索引赋值。

我们来看这样一段java代码

public void print(char[] cs,int[] s) {
    int i,j,k,x;
    x = 99;
    s[0] = 77;
}

编译后字节码如下

 public void print(char[], int[]);
    descriptor: ([C[S)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=7, args_size=3
         0: bipush        99                  //由于99的二进制为1100011,不超过8位,故使用bipush压入栈
         2: istore        6                   //将栈顶元素99弹出栈,并赋值给局部变量表第6个变量x
         4: aload_2                           //将局部变量第2个s数组入栈
         5: iconst_0                          //将整数索引0入栈
         6: bipush        77                  //将77入栈,同上面的99
         8: iastore                           //将77弹出栈,并赋值给局部变量表第2个数组s第0个索引
         9: return
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 9
      LocalVariableTable:                     //局部变量表
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/guanjian/calculate/test/Calc;
            0      10     1    cs   [C        //局部变量表第一个变量cs的char数组引用
            0      10     2     s   [I        //局部变量表第二个变量s的int数组引用
            4       6     6     x   I         //局部变量表第六个变量x为整数类型
    MethodParameters:
      Name                           Flags
      cs
      s

通用型操作

大部分数据操作指令是和数据类型相关的,但是无类型的指令还是有必要的,比如就栈操作而言,不是在所有时刻对栈的压入或者弹出都必须明确数据类型。通用型操作就提供了这种无需指明数据类型的操作。

nop 什么都不做

dup 将栈顶元素复制一份并再次压入栈顶,这样栈顶就有两份一模一样的元素了

pop 把一个元素从栈顶弹出,并且直接废弃

依然看Java代码

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

编译后的字节码如下

Constant pool:
   #1 = Methodref          #5.#30         // java/lang/Object."<init>":()V
   #2 = Integer            500000
   #3 = Long               50l
   #5 = Class              #31            // java/lang/Object
   #6 = Methodref          #5.#32         // java/lang/Object.toString:()Ljava/lang/String;
   #7 = Class              #33            // com/guanjian/calculate/test/Calc
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/guanjian/calculate/test/Calc;
  #15 = Utf8               calc
  #16 = Utf8               ()D
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               J
  #22 = Utf8               print
  #23 = Utf8               (I)V
  #24 = Utf8               i
  #25 = Utf8               obj
  #26 = Utf8               Ljava/lang/Object;
  #27 = Utf8               MethodParameters
  #28 = Utf8               SourceFile
  #29 = Utf8               Calc.java
  #30 = NameAndType        #8:#9          // "<init>":()V
  #31 = Utf8               java/lang/Object
  #32 = NameAndType        #34:#35        // toString:()Ljava/lang/String;
  #33 = Utf8               com/guanjian/calculate/test/Calc
  #34 = Utf8               toString
  #35 = Utf8               ()Ljava/lang/String;

public void print(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #5                  //new一个常量池#5的Object对象放入栈顶
         3: dup                               //将object对象复制一份出来重新入栈
         4: invokespecial #1                  //调用Object类的构造函数
         7: astore_2                          //将栈顶的object对象弹出栈并赋值给局部变量表第二个变量obj
         8: aload_2                           //将局部变量表第二个变量obj入栈
         9: invokevirtual #6                  //执行常量池第#6的toString()方法
        12: pop                               //将栈顶obj对象弹出栈废弃
        13: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 14: 13
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   Lcom/guanjian/calculate/test/Calc;
            0      14     1     i   I
            8       6     2   obj   Ljava/lang/Object;

注:pop指令只能丢弃1个字长(32位),如果要丢弃64位数据(long或者double),则需要使用pop2命令。如果要复制2个字长,则需要使用dup2指令。

类型转换指令

x2y 先将栈顶的x弹出,然后进行转换,转换后的y压入栈。x可能是i、f、l、d;y可能是i、f、l、d、c、s、b

java代码

public void print(int i) {
    long l = i;
    float f = l;
    int j = (int) l;
}

编译后的字节码

public void print(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=2
         0: iload_1                  //将局部变量表第一个整型变量i压入栈
         1: i2l                      //将栈顶i弹出转换成long类型压入栈
         2: lstore_2                 //将栈顶long类型变量弹出,赋值给局部变量表第二个long类型变量l
         3: lload_2                  //将局部变量表第二个long类型变量l压入栈
         4: l2f                      //将栈顶l弹出转换成float类型压入栈
         5: fstore        4          //将栈顶float类型变量弹出并赋值给局部变量表第4个变量f
         7: lload_2                  //将局部变量表第二个long类型变量l压入栈
         8: l2i                      //将栈顶l弹出转换成int类型压入栈
         9: istore        5          //将栈顶int类型变量弹出并赋值给局部变量表第5个整型变量j
        11: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 14: 7
        line 15: 11
      LocalVariableTable:            //局部变量表
        Start  Length  Slot  Name   Signature
            0      12     0  this   Lcom/guanjian/calculate/test/Calc;
            0      12     1     i   I
            3       9     2     l   J
            7       5     4     f   F
           11       1     5     j   I

在这些转换指令中只有i2b、i2c、i2s,但是没有b2i、c2i、s2i。也就是说,没有从byte、char或是short转换为其他数据类型的指令。来看一下如下代码

public void print(byte i) {
    int k = i;
    long l = i;
}

编译后的字节码

public void print(byte);
    descriptor: (B)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=5, args_size=2
         0: iload_1                  //将局部变量表第一个byte类型变量i压入栈
         1: istore_2                 //将栈顶变量i弹出赋值给局部变量表第2个int类型变量k
         2: iload_1                  //将局部变量表第一个byte类型变量i压入栈
         3: i2l                      //将栈顶变量i弹出转换成long类型压入栈,这里可以看到byte是当作int来识别的
         4: lstore_3                 //将栈顶long类型变量弹出并赋值给局部变量表第三个long类型变量l
         5: return
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/guanjian/calculate/test/Calc;
            0       6     1     i   B
            2       4     2     k   I
            5       1     3     l   J

我们可以看到byte类型转换成int类型,虚拟机并没有做实质性的转化处理,只是简单的通过操作数栈交换了两个数据。而将byte转换成long时,使用的是i2l,可以看到在内部byte在这里已经等同于int处理,类似的还有short。这种处理方式有两个特点

  1. 一方面可以减少实际的数据类型,如果为short和byte都准备一套指令,那么指令的数量就会大增,而虚拟机目前的设计上,只愿意使用一个字节表示指令,因此指令总数不能超过256个,为了节省指令资源,将short和byte当成int处理。
  2. 另一方面,由于局部变量表中的槽位固定为32位,无论是byte或者short存入局部变量表,都会占用32位空间。从这个角度来说,也没有必要区分这几种数据类型。

运算指令

运算指令为Java虚拟机提供了基本的加减乘除等运算功能,基本运行可以分为:加法、减法、乘法、除法、取余、数值取反、位运算、自增运算。

xadd 加法指令(x包括i、l、f、d)

xsub 减法指令

xmul 乘法指令

xdiv 除法指令

xrem 取余指令

xneg 数值取反

xinc 自增指令(x只能为i)

位运算指令

位移指令 ishl、ishr、iushr、lshl、lshr、lushr

xor 位或指令(x包括i、l)

xand 位与指令(x包括i、l)

xxor 位异或指令(第一个x包括i、l)

Java代码

public void print() {
    float i = 8;
    float j = -i;
    i = -j;
    int k = 33;
    k += 3;
    int l = k >> 1;
    int t = ~l;
}

编译后的字节码

Constant pool:                        //常量池
   #1 = Methodref          #7.#31         // java/lang/Object."<init>":()V
   #2 = Integer            500000
   #3 = Long               50l
   #5 = Float              8.0f
   #6 = Class              #32            // com/guanjian/calculate/test/Calc
   #7 = Class              #33            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lcom/guanjian/calculate/test/Calc;
  #15 = Utf8               calc
  #16 = Utf8               ()D
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               c
  #21 = Utf8               J
  #22 = Utf8               print
  #23 = Utf8               i
  #24 = Utf8               F
  #25 = Utf8               j
  #26 = Utf8               k
  #27 = Utf8               l
  #28 = Utf8               t
  #29 = Utf8               SourceFile
  #30 = Utf8               Calc.java
  #31 = NameAndType        #8:#9          // "<init>":()V
  #32 = Utf8               com/guanjian/calculate/test/Calc
  #33 = Utf8               java/lang/Object

public void print();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=6, args_size=1
         0: ldc           #5                  //将常量池#5的浮点数8.0f入栈
         2: fstore_1                          //将栈顶的8.0f弹出并赋值给局部变量表第一个浮点变量i
         3: fload_1                           //将局部变量表第一个浮点变量i入栈
         4: fneg                              //将浮点变量i的数值取反
         5: fstore_2                          //将栈顶i取反后的值弹出并赋值给局部变量表第二个浮点变量j
         6: fload_2                           //将局部变量表第二个浮点变量j入栈
         7: fneg                              //将浮点变量j的数值取反
         8: fstore_1                          //将栈顶j取反后的值弹出并赋值给局部变量表第一个浮点变量i
         9: bipush        33                  //将整数33压入栈
        11: istore_3                          //将栈顶的33弹出并赋值给局部变量表第三个整数变量k
        12: iinc          3, 3                //将局部变量表第三个整数变量k自增3
        15: iload_3                           //将局部变量表第三个整数变量k入栈
        16: iconst_1                          //将常量1入栈
        17: ishr                              //将栈中的两个元素都弹出,再把k做右位移1,再把结果k压入栈
        18: istore        4                   //将k右移后的结果从栈顶弹出并赋值给局部变量表第四个整型变量l
        20: iload         4                   //将局部变量表第四个整型变量l入栈
        22: iconst_m1                         //将常量-1入栈
        23: ixor                              //将栈中的两个元素都弹出,将变量l与-1进行异或操作取反,再把结果l压入栈
        24: istore        5                   //将取反后的l从栈顶出栈并赋值给局部变量表第五个整型变量t
        26: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 14: 6
        line 15: 9
        line 16: 12
        line 17: 15
        line 18: 20
        line 19: 26
      LocalVariableTable:                     //局部变量表
        Start  Length  Slot  Name   Signature
            0      27     0  this   Lcom/guanjian/calculate/test/Calc;
            3      24     1     i   F
            6      21     2     j   F
           12      15     3     k   I
           20       7     4     l   I
           26       1     5     t   I

注意:-1的二进制表示为全1的数字0xFF,任何数字与0xFF异或后,自然取反。

对象/数组操作指令

Java是面向对象的程序设计语言,虚拟机平台从字节码层面就对面向对象做了深层次的支持。有一系列指令专门用于对象操作,可进一步细分为创建指令、字段访问指令、类型检查指令、数组操作指令。

创建指令

new 用于创建普通对象。它接收一个操作数,为指向常量池的索引,表示要创建的类型,执行完成后,将对象的引用压入栈。

newarray 用于创建基本类型的数组

anewarray 用于创建对象数组

multianewarray 用于创建多维数组

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Oracle迁移MySQL 8特殊SQL处理 顶

    我们创建一个表,并生成两个表分区CUS_PART1,CUS_PART2.关于分区的分类可以参考https://www.cnblogs.com/wnlja/p/3...

    算法之名
  • Jedis运用scan删除正则匹配的key

    我们都知道用keys *进行查询key的时候会进行堵塞,导致redis整体不可用,而使用scan命令则不会.

    算法之名
  • 浅谈springboot Web模式下的线程安全问题

    我们在@RestController下,一般都是@AutoWired一些Service,由于这些Service都是单例,对于在Controller中调用他们的方...

    算法之名
  • 备忘录模式

    一、简介 1、备忘录 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先保存的状态。 2、模式成员 (...

    用户1215536
  • C++反汇编第六讲,认识C++中的Try catch语法,以及在反汇编中还原

          C++反汇编第六讲,认识C++中的Try catch语法,以及在反汇编中还原 我们以前讲SEH异常处理的时候已经说过了,C++中的Try catch...

    IBinary
  • 你是一个合格的管理员吗?

    2.账户名可以以字母 , 数字 , 英文句号 '.', 下划线 '_', 连字符 '-' 等连和使用

    DataScience
  • Node.js 抓取数据过程的进度保持

    最近自己有个批量调用 API 抓取数据的需求,类似爬虫抓数据的感觉。听到爬虫二字,我们常常想到的是 Python, Beautiful Soup 之流,而对于...

    zgq354
  • EasyNVR二次开发直播通道接口保活实例

    EasyNVR能够通过简单的网络摄像机通道配置,将传统监控行业里面的高清网络摄像机IP Camera、NVR等具有RTSP协议输出的设备接入到EasyNVR,E...

    EasyNVR
  • CNNVD最新漏洞

    今日CNNVD共发布安全漏洞48个,更新安全漏洞2个。主要影响厂商为美国Google(22个)、美国IBM(6个)、美国Linux(4个),主要影响产品为And...

    企鹅号小编
  • 什么是SAP OData Model Creator

    The SAP OData Model Creator is a web site where your OData service metadata is t...

    Jerry Wang

扫码关注云+社区

领取腾讯云代金券