2

前言

前面我们讲解了Class文件的结构、以及采用不同的方式来解读Class文件

  • 第一种是采用字节码一行一行的解读、
  • 第二种是采用javap的指令进行解读、
  • 第三使用第三方(idea、Eclipse)插件进解读、

但是针对于Class文件里方法的字节码指令,我们并没有进行细节的指令分析

本篇文章,我们开始对字节码指令进行分析,看看示例代码里的方法到底做了什么事情?

一、指令概述


Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。

由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数

我们可以采用上一篇文章的示例代码与字节码分析进行解析看看

image.png

我们根据上篇的思路,找找这些字节码指令对应的字节码是什么呢?代表什么意思呢?

image.png

image.png

image.png

虚拟机限制了Java 操作码的长度为一个字节(即0~255),这意味着操作码总数不可能超过256条

官方文档: https: //docs.oracle.com/javase/specs/jvms/se8/htm1/jvms-6.html

熟悉虚拟机的指令对于动态字节码生成、反编译Class文件、Class文件修补都有着非常重要的价值。因此阅读学节码作为了解ava虚拟机的基础技能,需要熟练掌握常见指令。

执行模型

================================

如果不考虑异常处理的话

那么Java虚拟机的解释器可以使用下面这个伪代码当做最基本的执行模型来理解

do{

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

字节码与数据类型

================================

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

  • iload指令用于从局部变量表中加载int型的数据到操作数栈中
  • fload指令用于从局部变量表中加载的则是float类型的数据

我们可以根据上篇的示例代码进行解析分析看看

image.png

我们可以看看局部变量表里的索引:0的值是什么?

image.png

对于大部分与数据类型相关的字节码指令,它们的操作码助记符中都有特殊的字符来表明专门为哪种数据类型服务:

  • 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类型作为运算类型

 byte b1 = 12;
 short s1 = 10
 int i = b1 + s1

指令的分类

================================

由于完全介绍和学习这些指令需要花费大量时间。为了让大家能够更快地熟悉和了解这些基本指令,这里将JVN中的字节码指令集按用途大致分成9类

  • 加载与存储指令
  • 算术指令
  • 类型转换指令
  • 对象的创建与访问指令
  • 方法调用与返回指令
  • 操作数栈管理指令
  • 比较控制指令
  • 异常处理指令
  • 同步控制指令

写在前面的,关于这些不同分类指令,大多在做值相关操作时:

一个指令可以从局部变量表、常量池、堆中对象、方法调用、系统调用中等取得数据,这些数据(可能是值,可能是对象的引用)被压入操作数栈。

一个指令也可以从操作数栈中取出一到多个值(pop多次),完成赋值、加减乘除、方法传参、系统调用等等操作。

二、加载与存储指令概述


作用

================================

加载和存储指令用于将数据从栈帧的局部变量表和操作数栈之间来回传递

常见指令

================================

image.png

上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如:iload_<n>)。

指令助记符实际上代表了一组指令

例如:iload_<n>代表了iload_0、iload_1、iload_2、iload_3这几个指令。

这几组指令都是某个带有一个操作数的通用指令(例如:iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中

除此之外它们的语义与原生的通用指令完全一致

例如 iload_0的语义与操作数为时的 iload 指令语义完全一致。

示例举例:

iload_0:将局部变量表中索引为0位置上的数据压入操作数栈中,这是占一个字节

iload 0:将局部变量表中索引为0位置上的数据压入操作数栈中,这是占两个字节

在尖括号之间的字母指定了指令隐含操作数的数据类型,具体信息如下:

  • <n>代表非负的整数、<i>代表是int类型
  • <l>代表long类型、<f>代表float类型
  • <d>代表double类型

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

三、加载与存储指令的再谈操作数栈与局部变量表


操作数栈(Operand Stacks)

================================

我们知道Java字节码是Java虚拟机所使用的指令集。因此与Java虚拟机基于栈的计算模型是密不可分

在解释执行过程中每当为Java方法分配栈桢时,Java虚拟机往往需要开辟一块额外的空间作为操作数栈,来存放计算的操作数以及返回结果

具体来说便是:执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中

image.png

以加法指令 iadd为例。假设在执行该指令前,栈顶的两个元素分别为 int值 1和 int 值 2,那么iadd 指令将弹出这两个int,并将求得的和int值 3 压入栈中。

image.png

由于 iadd 指令只消耗栈顶的两个元素,因此,对于离栈顶距离为?的元素,即图中的问号,iadd 指令并不关心它是否存在,更加不会对其进行修改。

局部变量表(Local Variables)

================================

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

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

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

image.png

举例:
public vid foo( long l,fl1oatf) {
    {
        int i = 0;
    }
    {
        string s = "He11o, wor1d" ;
    }
}

image.png

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

在方法执行时,虚拟机使用局部变量表完成方法的传递

四、加载与存储指令的局部变量入栈指令


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

这类指令大体可以分为:

  • 指令信息: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_e等指令。其中aload_n表示将个对象引用压栈

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

接下来使用示例代码来演示一下局部变量压栈指令

public class LoadAndStoreTest {

    //1.局部变量压栈指令
    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.print1n(flag);
        system.out.println(arr);
    }
}

我们使用编译一下,并且在idea中使用插件来看看该方法具体的指令有哪些?

image.png

此时我们根据这些指令进行分析看看,并且看看局部变量表与操作数栈是怎么样的情况

image.png
image.png

我们也可以使用idea的插件校验一下,看看是否方法里的局部变量表一致?
image.png

接下来我们分析一下指令是怎么操作局部变量表与操作数栈的,

image.png
image.png

当我们操作局部变量表索引为:5的时候,就会发现它占用了两个字节:iload 5,why?
image.png

五、、加载与存储指令的常量入栈指令


常量入栈指令的功能是

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

const系列

================================

用于对特定的常量入栈,入栈的常量隐含在指令本身里。

指令有: iconst_<i>、描述:i从-1到5
指令有: lconst_<l>、描述:l从0到1
指令有: fconst_<f>、描述:f从0到2
指令有: dconst_<d>、描述:d从0到1
指令有: aconst_null、描述:d从0到1

比如有示例:

  • 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。
如果要压入的元素是1ong或者double类型的,则使用1dc2_w指令,使用方式都是类似的。

接下来使用示例代码来演示一下常量压栈指令

public class LoadAndStoreTest {

    //2.常量入栈指令
    public void pushConstLdc() {
        int i = 1;
        int a = 5;
        int b = 6;
        int c = 127;
        int d = 128;
        int e = 32767;
        int f = 32768;
    }
}

我们使用编译一下,并且在idea中使用插件来看看该方法具体的指令有哪些?

image.png

虽然我们都是int类型的变量,但是指令里也有byte、long、short这些类型

所以我们可以总结一下,具体类型的范围具体定义,可以看如下图:

image.png

那么对于float、long类型,我们也进行示例代码看看具体是怎么样的?

public class LoadAndStoreTest {

    //2.常量入栈指令
    public void constLdc() {
        1ong a1 = 1;
        long a2 = 2;
        float b1 = 2;
        f1oat b2 = 3;
        double c1 = 1;
        double c2 = 2;
        Date d = null;
    }
}

我们使用编译一下,并且在idea中使用插件来看看该方法具体的指令有哪些?

image.png

我们前面也提到过压入的元素是1ong或者double类型的,则使用ldc2_w指令

当我们超出float类型的范围同样也是使用ldc2_w的指令

六、加载与存储指令的出栈指令


出栈装入局部变量表指令

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

这类指令主要以store的形式存在

指令:xstore ,描述:x为i、l、f、d、a
指令:xstore_n,描述:x 为i、l、f、d、a,n为至 3

其中指令istore_n将从操作数栈中弹出一个整数,并把它赋值给局部变量索引n位置

指令xstore由于没有隐含参数信息,故需要提供一个byte类型的参数类指定目标局部变量表的位置

接下来使用示例代码来演示一下常量压栈指令

public class LoadAndStoreTest {

    //3.出栈装入局部变量表指令
    public void store(int k,double d){
        int m = k + 2;
        long l = 12;
        string str = "atguigu";
        float f = 10.0F;
        d = 10;
    }
}

我们使用编译一下,并且在idea中使用插件来看看该方法具体的指令有哪些?

image.png

此时我们根据这些指令进行分析看看,并且看看出栈指令是怎么样的情况

image.png

image.png
image.png
image.png

接下来使用示例代码来演示一下其他的情况说明

public class LoadAndStoreTest {

    //4.出栈装入局部变量表指令
    public void foo( 1ong l,f1oat f){
        {
            int i = 0;
        }
        {
            string s = "He1lo,wor1d"
        }
    }
}

我们使用编译一下,并且在idea中使用插件来看看该方法具体的指令有哪些?

image.png

仅接着我们来看看局部变量表里有什么呢?

image.png

但是我们的局部变量表长度是多少呢?我们一起来看看

image.png

此时我们根据这些指令进行分析看看,并且看看出栈指令是怎么样的情况

image.png


28640
116 声望25 粉丝

心有多大,舞台就有多大