字节码指令集与解析

一、概述

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

Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的数字(称为操作码:Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数:Operands)构成。由于Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。

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

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

执行模型

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

字节码与数据类型

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

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

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

也有一些指令的助记符中没有明确地指明操作类型的字母,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类型作为运算类型。

指令分析

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

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

(说在前面)在做值相关操作时:

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

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

二、加载与存储指令

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

常用指令

1)【局部变量压栈指令】:将一个局部变量加载到操作数栈

xload、xload_<n>(其中x为i、l、f、d、a,n为0为到3)

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

bipush、 sipush、

ldc、ldc_w、ldc2_w、

aconst_null、 iconst_m1、 iconst_<i>、lconst<l>、fconst_<f>、 dconst<d>

3)【出栈装入局部变量表指令】:将一个数值从操作数栈存储到局部变量表

xstore、 xstore_<n>(其中x为i、l、f、d、a,n为0到3);

xastore(其中x为、l、f、d、a、b、c、s)

4)【扩充局部变量表的访问索引的指令】: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类型的指令来表示。

复习:再谈操作数栈与局部变量表

1、局部变量压栈指令

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

这类指令大体可以分为:

xload_<n>(x为i、l、f、d、a,n0为到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);
 }

2、常量入栈指令

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

指令const系列

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

iconst_<i>(i从-1到5)

lconst_<l>(l从0到1)

fconst_<f>(f从0到2)

dconst_<d>(d从到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系列

主要包括 bipusha和 sipush。

它们的区别在于接收数据类型的不同, bipush接收8位整数作为参数,sipush接收16位整数,它们都将参数压入栈。

指令ldc系列

如果以上指令都不能满足需求,那么可以使用万能的ldc指令,它可以接收一个8位的参数,该参数指向常量池中的int、float或者 Stringl的索引,将指定的内容压入堆栈。

类似的还有ldc_w,它接收两个8位参数,能支持的索引范围大于ldc。

如果要压入栈的元素是long或double类型的,则使用ldc_2w指令,使用方式都是类似的。

总结

举例解释

    public void pushConstLdc(){
        int i = -1;
        int a = 5;
        int b = 6;
        int c = 127;
        int d = 128;
        int e = 32767;
        int f = 32768;
    }

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

注意:常量入栈指令中的n和局部变量压栈指令中的n不一样,本次的n代表数值或者对象,而不是局部变量表中的下标。

https://blog.youkuaiyun.com/qq_51409098/article/details/126780602

常用字节码指令

以上两种方式,都挺方便的,根据自己喜好自由选择就好了。

相信你肯定也被很多字节码指令搞的一头雾水过,接下来,我们就一起学习下常用的指令都有哪些吧。之后,再来看上边的例子就轻松很多了。

数据类型

我们知道 Java 是强类型语言,在使用之前肯定已经确定了它的类型。而数据类型,无非就是基本数据类型和引用类型。它们对应的字节码,其实就是用它们的对应类型的英文首字母来表示的。(引用类型除外)

例如,s 代表 short , i 代表 int, l 代表 long ,f 代表 float,d 代表 double,b 代表 byte(不包括boolean),c 代表 char,a 代表 reference 引用。

加载和存储指令

我们知道,一个方法的运行,会在栈的栈帧中执行。方法中的变量称为局部变量,数的操作需要用到操作数栈。因此,加载和存储指令,就是数据在局部变量表和操作数栈中来回传输。

将一个局部变量加载到操作数栈:如 iload、iload_ 。表示加载的是 int 类型变量。

iload_ 后边带数字代表第几个 int 型变量。例如 iload_0 代表把第一个 int 型局部变量加载到操作数栈。

其他类型变量同上:lload ,fload ,dload,aload 。

将一个数值存储到局部变量表:istore,istore_,lstore,lstore_,fstore,fstore_,dstore,dstore_,astore,astore_ 。

将一个常量加载到操作数栈:aconst_null,iconst_m1,iconst_,lconst_,fconst_,dconst_ ,bipush,sipush。

ldc 代表把 int、float、String类型常量从常量池中加载到操作数栈。ldc_w 代表宽索引。

ldc2_w 代表把 long 或 double 类型常量从常量池中加载到操作数栈。(宽索引)

ldc_w和ldc2_w属于​​宽索引指令​​​,即指令对应的(索引值)参数为2个字节。而ldc指令对应的(索引值)参数为1个字节。

当运行时常量池中的常量个数超过256个(1个字节所能代表的数量)时,需要使用支持2个字节索引值的指令ldc_w指令来代替ldc访问常量池。

这里需要说的是,int 类型根据数值的取值范围不同,而采用不同的字节码指令。

iconst_m1 代表 -1 ,iconst_ 代表 0~5 。bipush 代表 -128~127(byte取值范围), sipush 代表 -32768~32767(short取值范围),ldc 在 int 中代表 -2147483648~2147483647(int取值范围)。

public class TestByteCode {
    public static void main(String[] args) {
        int a = -1;
        int b = 0;
        int c = 1;
        int d = 2;
        int e = 3;
        int f = 4;
        int g = 5;
        int h = 127;
        int i = 32767;
        int j = 2147483647;
    }
}
其字节码为:
0 iconst_m1
 1 istore_1
 2 iconst_0
 3 istore_2
 4 iconst_1
 5 istore_3
 6 iconst_2
 7 istore 4
 9 iconst_3
10 istore 5
12 iconst_4
13 istore 6
15 iconst_5
16 istore 7
18 bipush 127
20 istore 8
22 sipush 32767
25 istore 9
27 ldc #2 <2147483647>
29 istore 10
31 return 

访问指令

访问类字段:getstatic,putstatic

访问类实例字段:getfield,putfield

方法调用和返回

invokevirtual :用于调用对象的实例方法。

invokeinterfce:用于调用接口方法。

invokespecial:用于调用一些特殊的方法,如父类构造方法,实例初始化方法,私有方法。

invokestatic:用于调用类的静态方法。

invokedynamic:用于调用动态方法。

方法返回是跟返回类型相关,根据不同的返回类型,有不同的指令。

return:返回 void。

ireturn:注意,这个不止返回 int ,返回 boolean,byte,char,short 也用这个指令。其实,很多指令都没有直接支持 byte,char,short和 boolean,而是用 int 类型代替。这是因为,虚拟机的操作码长度只有一个字节,只能表示有限个数的指令。(我们这里所提到的所有指令,都只是方便我们记忆的助记符,而在计算机内部肯定还是一个字节,即 8 个 bit 位的二进制)

lreturn:返回 long 类型。

freturn:返回 float 类型。

dreturn:返回 double 类型。

areturn:返回引用类型。

其余字节码指令

上边介绍的指令只是很少一部分字节码指令。但是,麻雀虽小,五脏俱全,也包括了最基本的变量定义,调用方法,和方法返回这些最基本的功能。同时,也足够我们去解释上边的问题,为什么注释行会被执行了。

更多的字节码指令可以参考《深入理解Java虚拟机》这本书的 6.4 节,和最后的附录字节码指令表。需要这本书的,可以在公众号后台回复“Java虚拟机”获取。

由于字节码指令太多,比如还有运算指令,包括加减乘除、位运算,比较指令等,if 等控制指令,类型强转指令,还有多线程用到的同步锁。And so on ~

不可能把它们全部记住背会,但是,其实都是由规律可循的,很多都是见名知意,用各种英文首字母简写代表。比如,int类型的加法运算,就是 iadd,double 类型的减法运算是 dsub。等等。

因此,我这里只是给了一个引子,重要的还是需要自己去寻找方法,不断的实践探索 ~

另外,官方文档才是最好的学习途径:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

注释行字节码

最后,学以致用,实践出真知。我们一起看一下开头抛出的问题吧,为什么注释行会被执行。看一下它的字节码指令就知道了。

public class TestAnno {
    public static void main(String[] args) {
        String str = "张三";
        // \u000d str="李四";
        System.out.println(str);
    }
}
//这里就是从常量池中取出一个字符串“张三”,然后加载到操作数栈
 0 ldc #2 <张三>
 //从操作数栈把这个字符串引用存储到局部变量表中
 2 astore_1
 //从常量池中取出一个字符串“李四”,然后加载到操作数栈
 3 ldc #3 <李四>
 //可以发现和上边一样都是astore_1,说明指向的是同一个引用 str。
 //若定义另外一个str2赋值给“李四”,这个操作指令肯定就不同了。
 5 astore_1
 //访问的是System类的静态属性out,我们知道它的类型是PrintStream
 6 getstatic #4 <java/lang/System.out>
 //将out这个引用加载到操作数栈栈顶,以便后边操作它
 9 aload_1
 //调用out的实例方法 println ,用于打印结果到控制台
 10 invokevirtual #5 <java/io/PrintStream.println>
 //main方法返回值为void
 13 return

这里就不用再多余解释了吧,可以从字节码指令中看到,后边的 ​​str="李四"​​ 被编译器执行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值