0
点赞
收藏
分享

微信扫一扫

十八、JVM-字节码指令与解析举例(基础篇)

一、概述

1、概述

  • Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。
  • Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码:Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数:Operands)构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。
  • 由于限制了Java虚拟机操作码的长度为一个字节(即0 ~ 255),这意味着指令集的操作码总数不可能超过256条。
  • 熟悉虚拟机的指令对于动态字节码生成、反编译Class文件、Class文件修补都有着非常重要的价值。因此,阅读字节码作为了解Java虚拟机的基础技能,需要熟练掌握常见指令。

2、执行模型

如果不考虑异常处理的话,那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解

do{
自动计算pc寄存器的值加1;
根据PC寄存器指示的位置,从字节码流中取出操作码;
if(字节码存在操作数)从字节码流中取出操作数;
执行操作码所定义的操作
}while(字节码流的长度>0)

3、字节码与数据类型

  • 在Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。

    • 例如,iload指令用于从局部变量表中加载int类型的数据到操作数栈中,而fload指令加载的则是float类型的数据。
  • 对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:

    • i代表对int类型的数据操作
    • l代表long
    • s代表short
    • b代表byte
    • c代表char
    • f代表float
    • d代表double
  • 也有一些指令的助记符中没有明确地指明操作类型的字母,arraylength指令,它没有代表数据类型的特殊字符,但操作数永远只能是一个数组类型的对象。

  • 还有另外一些指令, 如无条件跳转指令goto则是与数据类型无关的。

  • 大部分的指令都没有支持整数类型byte、 char和short,甚至没有任何指令支持boolean类型。 编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend) 为相应的int类型数据,将boolean和char 类型数据零位扩展(Zero-Extend)为相应的int类型数据。与之类似,在处理boolean、byte、 short 和char类型的数组时,也会转换为使用对应的int类型的字节码指令来处理。因此,大多数对于boolean、byte、 short 和char类型数据的操作,实际上都是使用相应的int类型作为运算类型。

4、指令分类

  • 由于完全介绍和学习这些指令需要花费大量时间。为了让大家能够更快地熟悉和了解这些基本指令,这里将JVM的字节码指令集按用途大致分成9类。
    • 加载与存储指令
    • 算术指令
    • 类型转换指令
    • 对象的创建与访问指令
    • 方法调用与返回指令
    • 操作数栈管理指令
    • 比较控制指令
    • 异常处理指令
    • 同步控制指令
    • (说在前面)在做值相关操作时:
  • 一个指令,可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值可能是对象的引用)被压入操作数栈。
  • 一个指令, 也可以从操作数栈中取出一到多个值(pop多次),完成赋值、 加减乘除、方法传参、系统调用等等操作。

二、加载与存储指令

1、作用

用于将数据从栈帧的局部变量表和操作数栈之间来回传递

2、常用指令

  • 【局部变量压栈指令】:将一个局部变量加载到操作数栈:xload、xload_<n>(其中x为i、l、f、d、a,n为0为到3)

  • 【常量入栈指令】 :将一个常量加载到操作数栈

    • bipush、 sipush、
    • ldc、ldc_w、ldc2_w、
    • aconst_null、 iconst_m1、 iconst_<i>、lconst<l>、fconst_<f>、 dconst<d>
  • 出栈装入局部变量表指令】:将一个数值从操作数栈存储到局部变量表

    • xstore、 xstore_<n>(其中x为i、l、f、d、a,n为0到3);
    • xastore(其中x为、l、f、d、a、b、c、s)
  • 【扩充局部变量表的访问索引的指令】:wide

  • 上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload_<n>),这些指令助记符实际上代表了一组指令。例如 iload_<n>代表了iload_0、iload_1、iload_2和 iload_3这几个指令。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。

  • 除此之外,它们的语义与原生的通用指令完全一致。例如 iload_0的语义与操作数为0时的iload指令语义完全一致。在尖括号之间的字母指定了指令隐含操作数的数据类型,<n>代表非负的整数,<i>代表是int类型数据,<l>代表long类型,<f>代表float类型,<d>代表 double类型。

  • 操作byte、char、short和boolean类型数据时,经常用int类型的指令来表示。

iload_0:局部变量表中索引为0位置上的数据压入操作数栈中。 iload 0:局部变量表中索引为0位置上的数据压入操作数栈中。字节码文件中,操作码占一个字节,操作数占2个字节,所以此操作需要占2个字节,而iload_0只需要占一个字节,更节约空间。 而不能无限制的用下划线制造操作码,256个不同的种类很快被占慢,而0-3使用的比较多,所以用下划线的方式指定出来

3、再谈操作数栈与局部变量表

1、操作数栈

我们知道,Java字节码是Java虚拟机所使用的指令集。因此,它与Java虚拟机基于栈的计算模型是密不可分的。在解释执行过程中,每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果。 具体来说便是:执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。 419.png 以加法指令iadd为例。假设在执行该指令前,栈顶的两个元素分别为int值1和int值2,那么iadd指令将弹出这两个int,并将求得的和int值3压入栈中。 420.png 由于iadd指令只消耗栈顶的两个元素,因此,对于离栈顶距离为2的元素,即图中的问号,iadd 指令并不关心它是否存在,更加不会对其进行修改。 422.png

2、局部变量表(Local Variables)

Java方法栈桢的另外一个重要组成部分则是局部变量区,字节码程序可以将计算的结果缓存在局部变量区之中。

实际上,Java虚拟机将局部变量区当成一个数组,依次存放this指针(仅非静态方法),所传入的参数,以及字节码中的局部变量。

和操作数栈一样,long类型以及double类型的值将占据两个单元,其余类型仅占据一个单元。 421.png 举例:

public void foo(long l, float f) {
    {
        int i = e;
    }
    {
        String s = "Hello, World";
    }
}

423.png this表示当前类的引用,l和f的类型的值占两个槽位,i和s变量由于分别在各自代码块中,没有共同的生命周期,所以占同一个槽位(即槽位复用)

在栈帧中,与性能调优关系最为密切的部分就是局部变量表。局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

4、局部变量压栈指令

iload 从局部变量中装载int类型值
 
lload 从局部变量中装载long类型值
 
fload 从局部变量中装载float类型值
 
dload 从局部变量中装载double类型值
 
aload 从局部变量中装载引用类型值(refernce)
 
iload_0 从局部变量0中装载int类型值
 
iload_1 从局部变量1中装载int类型值
 
iload_2 从局部变量2中装载int类型值
 
iload_3 从局部变量3中装载int类型值
 
lload_0 从局部变量0中装载long类型值
 
lload_1 从局部变量1中装载long类型值
 
lload_2 从局部变量2中装载long类型值
 
lload_3 从局部变量3中装载long类型值
 
fload_0 从局部变量0中装载float类型值
 
fload_1 从局部变量1中装载float类型值
 
fload_2 从局部变量2中装载float类型值
 
fload_3 从局部变量3中装载float类型值
 
dload_0 从局部变量0中装载double类型值
 
dload_1 从局部变量1中装载double类型值
 
dload_2 从局部变量2中装载double类型值
 
dload_3 从局部变量3中装载double类型值
 
aload_0 从局部变量0中装载引用类型值
 
aload_1 从局部变量1中装载引用类型值
 
aload_2 从局部变量2中装载引用类型值
 
aload_3 从局部变量3中装载引用类型值
 
iaload 从数组中装载int类型值
 
laload 从数组中装载long类型值
 
faload 从数组中装载float类型值
 
daload 从数组中装载double类型值
 
aaload 从数组中装载引用类型值
 
baload 从数组中装载byte类型或boolean类型值
 
caload 从数组中装载char类型值
 
saload 从数组中装载short类型值

1、局部变量压栈常用指令集

xload_n xload_ xload_1 xload_2 xload_3
<b>iload_n</b> iload_ iload_1 iload_2 iload_3
<b>lload_n</b> lload_ lload_1 lload_2 lload_3
<b>fload_n</b> fload_ fload_1 fload_2 fload_3
<b>dload_n</b> dload_ dload_1 dload_2 dload_3
<b>aload_n</b> aload_ aload_1 aload_2 aload_3

2、局部变量压栈指令剖析

局部变量压栈指令将给定的局部变量表中的数据压入操作数栈。 这类指令大体可以分为:

  • xload_<n>(x为i、l、f、d、a,n为0到3)
  • xload(x为i、l、f、d、a) 说明:在这里,x的取值表示数据类型。

指令xload_n表示将角标为n的局部变量压入操作数栈,比如iload_1、fload_0、aload_0等指令。其中aload_n表示将一个对象引用压栈。

指令xload通过指定参数的形式,把局部变量压入操作数栈,当使用这个命令时,表示局部变量的数量可能超过了4个,比如指令iload、fload等。 实验:

public void load(int num, Object obj, long count, boolean flag, short[] arr) {
    System.out.println(num);
    System.out.println(obj);
    System.out.println(count);
    System.out.println(flag);
    System.out.println(arr);
}

424.png

5、常量入栈指令

aconst_null 将null对象引用压入栈
 
iconst_m1 将int类型常量-1压入栈
 
iconst_0 将int类型常量0压入栈
 
iconst_1 将int类型常量1压入栈
 
iconst_2 将int类型常量2压入栈
 
iconst_3 将int类型常量3压入栈
 
iconst_4 将int类型常量4压入栈
 
iconst_5 将int类型常量5压入栈
 
lconst_0 将long类型常量0压入栈
 
lconst_1 将long类型常量1压入栈
 
fconst_0 将float类型常量0压入栈
 
fconst_1 将float类型常量1压入栈
 
dconst_0 将double类型常量0压入栈
 
dconst_1 将double类型常量1压入栈
 
bipush 将一个8位带符号整数压入栈
 
sipush 将16位带符号整数压入栈
 
ldc 把常量池中的项压入栈
 
ldc_w 把常量池中的项压入栈(使用宽索引)
 
ldc2_w 把常量池中long类型或者double类型的项压入栈(使用宽索引)

1、常量入栈常用指令集

xconst_n 范围 xconst_null xconst_m1 xconst_ xconst_1 xconst_2 xconst_3 xconst_4 xconst_5
iconst_n [-1, 5] iconst_m1 iconst_ iconst_1 iconst_2 iconst_3 iconst_4 iconst_5
lconst_n 0, 1 lconst_ lconst_1
fconst_n 0, 1, 2 fconst_ fconst_1 fconst_2
dconst_n 0, 1 dconst_ dconst_1
aconst_n null, String literal, Class literal aconst_null
bipush 一个字节,2^8^,[-2^7^, 2^7^ - 1],即[-128, 127]
sipush 两个字节,2^16^,[-2^15^, 2^15^ - 1],即[-32768, 32767]
ldc 四个字节,2^32^,[-2^31^, 2^31^ - 1]
ldc_w 宽索引
ldc2_w 宽索引,long或double

2、常量入栈指令剖析

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

  • 指令const系列:用于对特定的常量入栈,入栈的常量隐含在指令本身里。指令有:iconst_<i>(i从-1到5)、lconst_<l>(1从0到1)、fconst_<f>(f从0到2)、dconst_<d>(d从0到1)、aconst_null。比如,

    • iconst_m1将-1压入操作数栈;
    • iconst_x(x为0到5)将x压入栈;
    • lconst_0、lconst_1分别将长整数0和1压入栈;
    • fconst_0、fconst_1、fconst_2分别将浮点数0、1、2压入栈;
    • dconst_0和dconst_1分别将double型0和1压入栈;
    • aconst_null将null压入操作数栈;
  • 从指令的命名上不难找出规律,指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数,d表示双精度浮点,习惯上用a表示对象引用。如果指令隐含操作的参数,会以下划线形式给出。

  • 指令push系列:主要包括bipush和sipush。它们的区别在于接收数据类型的不同,bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈。

  • 指令ldc系列:如果以上指令都不能满足需求,那么可以使用万能的

    • ldc指令,它可以接收一个8位的参数,该参数指向常量池中的int、float或者String的索引,将指定的内容压入堆栈。
    • 类似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc。
    • 如果要压入的元素是long或者double类型的,则使用ldc2_w指令,使用方式都是类似的 总结如下: 425.png 实验1:
    public void pushConstLdc(){
        int i = -1;
        int a = 5;
        int b = 6;
        int c = 127;
        int d = 128;
        int e = 32767;
        int f = 32768;
    }

426.png

1、iconst_n中n的范围是从-1~5,其中-1的写法是m1,当n>5时,就要用bipush n了 2、bipush n中的n从-128~127,当n>127时或者n<-128时,就需要使用sipush n了 3、sipush n中的n从-32768~32767,当n>32767或者n<-32768的时候,就需要使用ldc了 4、ldc常量池地址,这个没有大小限制

实验2:

    public void constLdc(){
        long a1 = 1;
        long a2 = 2;
        float b1 = 2;
        float b2 = 3;
        double c1 = 1;
        double c2 = 2;
        Date d = null;
    }

427.png

1、lconst_n中的n只能是1或者2,不在这个范围就得使用ldc2_w地址,普通的使用ldc就可以了,但是long和double类型就使用ldc2_w,因此,下面的fconts_n,dconts_n就不再解释了,都根据自己的范围选择合适的操作符。 上面粉色的数值是局部变量表的下标,其中long和double占据两个槽位,所以istore_3后面就直接是istore_5了

三、算数指令

四、类型转换指令

五、对象的创建与访问指令

六、方法调用与返回指令

七、操作数栈管理指令

八、控制转移指令

九、异常处理指令

十、同步控制指令

举报

相关推荐

0 条评论