前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java字节码 顶

Java字节码 顶

作者头像
算法之名
发布2020-05-26 14:52:34
7970
发布2020-05-26 14:52:34
举报
文章被收录于专栏:算法之名

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类型

假设我们写一个这样的类

代码语言:javascript
复制
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

获得如下内容

代码语言:javascript
复制
警告: 二进制文件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为常量池。其中有这么几行字节码指令

代码语言:javascript
复制
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的这样一个方法

代码语言:javascript
复制
public void print(char[] cs,short[] s) {
    System.out.println(s[0]);
    System.out.println(cs[0]);
}

编译后的字节码为

代码语言:javascript
复制
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代码

代码语言:javascript
复制
public void print(char[] cs,int[] s) {
    int i,j,k,x;
    x = 99;
    s[0] = 77;
}

编译后字节码如下

代码语言:javascript
复制
 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代码

代码语言:javascript
复制
public void print(int i) {
    Object obj = new Object();
    obj.toString();
}

编译后的字节码如下

代码语言:javascript
复制
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代码

代码语言:javascript
复制
public void print(int i) {
    long l = i;
    float f = l;
    int j = (int) l;
}

编译后的字节码

代码语言:javascript
复制
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转换为其他数据类型的指令。来看一下如下代码

代码语言:javascript
复制
public void print(byte i) {
    int k = i;
    long l = i;
}

编译后的字节码

代码语言:javascript
复制
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代码

代码语言:javascript
复制
public void print() {
    float i = 8;
    float j = -i;
    i = -j;
    int k = 33;
    k += 3;
    int l = k >> 1;
    int t = ~l;
}

编译后的字节码

代码语言:javascript
复制
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 用于创建多维数组

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档