一、概述
每个方法被执行的时候,Java 虚拟机都会同步创建一个栈帧用于存放局部变量表、操作数栈、动态链接、方法出口等信息,在栈帧中与 Jvm 指令关系最密切的就是局部变量表和操作数栈,所以在介绍 Jvm 指令之前,我们先了解一下栈帧中最重要的两个内存区域
1.1、操作数栈
Jvm 是基于堆栈结构模型的虚拟机,每个 Java 方法都对应一个栈帧模型,在栈帧中会专门开辟出一块空间作为操作数栈,它的作用就是用来存放当前指令所涉及到的操作数和指令执行完成之后的结果
简单来说主要涉及到以下 2 个阶段
执行指令之前: 需要将该指令涉及到的操作数压入操作数栈栈顶的位置
执行指令时: 需要将操作数栈中的操作数弹出,并根据指令的不同对操作数做相应的计算操作,最后将计算的结果又重新压入操作数栈中
1.2、局部变量表
局部变量表存放了编译期可知的各种 Jvm 的基本数据类型(byte、short、char、int、long、float、double、boolean)、对象类型(一个指向对象起始地址位置的指针)、returnAddress 类型(指向了一条字节码指令的地址)
这些数据类型在局部变量表中的存储空间以局部变量槽(slot)来表示,每一个局部变量槽所占用的内存大小是 4 个字节,对于 64 位长度的 long、double 类型的数据它们会占用两个变量槽,其余的数据类型都只占用一个.局部变量表所需的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的即在方法运行期间不会改变局部变量槽的数量
案例
public void method(byte b, short s, char c, int i, float f, double d, long l, boolean bool, String str) { System.out.println(b); System.out.println(s); System.out.println(c); System.out.println(i); System.out.println(f); System.out.println(d); System.out.println(l); System.out.println(bool); System.out.println(str); }
上述代码对应栈帧中的局部变量表分布如下,其中 double 类型的变量 d、long 型的变量 l 占据两个槽位
通过 javap -v -p 反解析进行验证
该方法总共有 12 个槽位,由于是非静态方法,所以第一个槽位是 this 指针(这也是为什么可以在非静态方法中使用 this 关键字的原因),byte、short、char、int、float、boolean、引用类型占据一个槽位,而 double、long 则占据两个槽位,其中有一点需要注意的是,引用类型的签名是 L 开头的
二、局部变量压栈指令
这类指令可以分为以下两个格式
xload_n: x 代表数据类型,取值有 i、f、d、l、a; n 代表局部槽位,取值有 0、1、2、3
xload n: x 代表数据类型,取值为 i、f、d、l、a; n 代表局部槽位索引下标,取值范围为大于等于 4 的正整数
案例
public void method(byte b, short s, char c, int i, float f, double d, long l, boolean bool, String str) { System.out.println(this); System.out.println(b); System.out.println(s); System.out.println(c); System.out.println(i); System.out.println(f); System.out.println(d); System.out.println(l); System.out.println(bool); System.out.println(str); }
使用 jclasslib 反解析结果如下
通过反解析后的结果可以得知
byte、short、char、boolean、int 类型的变量从局部变量表加载到操作数栈的过程中,统一使用 int 类型
当加载的变量超过 4 个(即 xload_0、xload_1、xload_2、xload_3 已经使用完时),使用 xload n 这种格式表示剩余的局部变量
在栈帧中,与性能调优关系最为密切的部分就是局部变量表,局部变量表中的变量也是重要的垃圾回收根节点(GcRoots),只要被局部变量表中的变量直接或者间接应用的对象都不会被垃圾回收器回收
三、常量入栈指令
常量入栈指令的功能是将常数压入操作数栈,根据数据类型和入栈常量取值范围的不同,又可以区分为 const、push、ldc 指令系列
const 指令系列
用于对特定的常量值入栈,入栈的常量值隐含在指令本身当中,所有的指令包括 iconst_i (-1 <= i <= 5)、fconst_f (0 <= f <= 2)、dconst_d (0 <= d <= 1)、lconst_l (0 <= f <= 1)、aconst_null(引用类型)
iconst_m1: 将(byte / short / char / int / boolean) 类型的常数 -1 压入操作数栈中
iconst_0、iconst_1、iconst_2、iconst_3、iconst_4、iconst_5: 将 (byte / short / char / int / boolean) 类型的常数 0、1、2、3、4、5 压入操作数栈中
fconst_0、fconst_1、fconst_2: 将 float 类型的常数 0、1、2 压入操作数栈当中
dconst_0、dconst_1: 将 double 类型的常数 0、1 压入操作数栈当中
lconst_0、lconst_1: 将 long 类型的常数 0、1 压入操作数栈当中
aconst_null: 将引用类型的常数 null 压入操作数栈中
push 指令系列
包含 bipush、sipush,这两个指令是为了表示(byte、short、char、int) 类型的数据
(ps: 这里的数据类型不包括 boolean 类型,是由于 boolean 的取值只有 false(0)、true(1),这两个值可以使用指令 iconst_0 和 iconst_1 来表示)
如果 byte、short、char、int 类型的常数 x 的取值范围 -128 <= x <= -2 或者 6 <= x <= 127,那么就是用 bipush x 的形式来表示
如果 byte、short、char、int 类型的常数 x 的取值范围 -32768 <= x <= -129 或者 128 <= x <= 32767,那么就是用 sipush x 的形式来表示
ldc 指令系列
如果以上指令都不能满足需求,那么可以使用 ldc 指令,它可以接收一个 8 位的参数,该参数指向常量池中的 int、float、引用类型的索引,将指定的内容压入操作数栈中
类似的还有 ldc_w,它接收两个 8 位的参数,支持的索引范围大于 ldc
如果要入栈的元素是 long、double 类型的,则使用 ldc2_w
各种数据类型的常数压入操作数栈对应的列表如下
数据类型 | 常数压入操作数栈对应 Jvm 指令 | 常数取值范围 |
int(byte、short、char、int、boolean) | iconst_m1 (m1 代表 -1) | x = -1 |
iconst_x (x 为操作数的值) | 0 <= x <= 5 | |
bipush_x | -128 <= x <= -2 或 6 <= x <= 127 | |
sipush_x | -32768 <= x <= -129 或 128 <= x <= 32767 | |
ldc | x <= -32769 或 x >= 32768 | |
float | fconst_x | x = [0、1、2] |
ldc | x <= -1 或 x >= 3 | |
double | dconst_x | x = [0、1] |
ldc | x <= -1 或 x >= 2 | |
long | lconst_x | x = [0、1] |
ldc | x <= -1 或 x >= 2 | |
reference | aconst_null | null |
ldc | String literal、Class literal |
案例
public void method() { byte b = -1; short s = 0; char c = 5; boolean bool = true; int i1 = -128; int i2 = 127; int i3 = 128; int i4 = -32768; int i5 = 32767; int i6 = 32768; int i7 = -32769; int i8 = 100000; }
字节码指令解释
// 对应 byte b = -1,byte 在字节码层面是以 int 类型来表示的,iconst_m1 代表将 byte 类型的常数 -1 压入操作数栈 0 iconst_m1 1 istore_1 // 对应 short s = 0,short 在字节码层面是以 int 类型来表示的,iconst_0 代表将 short 类型的常数 0 压入操作数栈 2 iconst_0 3 istore_2 // 对应 char c = 5,char 在字节码层面是以 int 类型来表示的,iconst_5 代表将 char 类型的常数 5 压入操作数栈 4 iconst_5 5 istore_3 // 对应 boolean bool = true,boolean 在字节码层面是以 int 类型来表示的,true 代表的是 1,iconst_1 代表将 boolean 类型的常数 1 压入操作数栈 6 iconst_1 7 istore 4 // 对应 int i1 = -128,由于 -128 在 byte 类型范围之内所以使用 bipush,bipush -128 代表将 int 类型的常数 -128 压入操作数栈 9 bipush -128 11 istore 5 // 对应 int i2 = 127,由于 127 在 byte 类型范围之内所以使用 bipush,bipush 127 代表将 int 类型的常数 127 压入操作数栈 13 bipush 127 15 istore 6 // 对应 int i3 = 128,由于 128 已经超出了 byte 类型范围,但是在 short 类型范围内所以使用 sipush,sipush 128 代表将 int 类型的常数 128 压入操作数栈 17 sipush 128 20 istore 7 // 对应 int i4 = -32768,由于 -32768 在 short 类型范围内所以使用 sipush,sipush 32768 代表将 int 类型的常数 32768 压入操作数栈,这里为什么使用正数 32768,而不是直接使用 -32768,可能会在操作数栈中运算时会做取反操作 22 sipush 32768 25 istore 8 // 对应 int i5 = 32767,sipush 32767 代表将 int 类型的常数 32767 压入操作数栈 27 sipush 32767 30 istore 9 // 对应 int i6 = 32768,由于 128 已经超出了 short 类型范围所以使用 ldc 指令,ldc #2 代表将常量池中第 2 个位置的常量(32768) 压入操作数栈 32 ldc #2 <32768> 34 istore 10 // 对应 int i7 = -32769,由于 -32769 已经超出了 short 类型范围所以使用 ldc 指令,ldc #3 代表将常量池中第 3 个位置的常量(-32769) 压入操作数栈 36 ldc #3 <-32769> 38 istore 11 // 对应 int i8 = 100000,由于 100000 已经超出了 short 类型范围所以使用 ldc 指令,ldc #4 代表将常量池中第 4 个位置的常量(100000) 压入操作数栈 40 ldc #4 <100000> 42 istore 12 44 return
注意: 常量入栈指令和局部变量入栈指令的操作数所代表的含义是不同的,常量入栈指令的操作数代表的是常数的数值或者是对应的引用地址值,而局部变量入栈的操作数指的是局部变量的槽位索引
四、弹出操作数栈存入局部变量表指令
出栈装入局部变量表指令是将操作数栈栈顶的元素弹出后,装入局部变量表的指定位置,用于给局部变量赋值
这类指令主要以 store 的形式存在,store 指令和 load 指令类似有下面两种格式
xstore_n: x 代表数据类型,取值有 i、f、d、l、a; n 代表局部槽位,取值有 0、1、2、3
xstore n: x 代表数据类型,取值为 i、f、d、l、a; n 代表局部槽位索引下标,取值范围为大于等于 4 的正整数
例如:
istore_0、istore_1、istore_2、istore_3: 将操作数栈栈顶 int 类型的数据弹出操作数栈,然后将其装入局部变量表槽位下标为 0、1、2、3 的位置
dstore 5: 将操作数栈栈顶 double 类型的数据弹出操作数栈,然后将装入局部变量表槽位下标为 5 的位置
下面以一段代码来简单分析一下
public void method(int i, double d) { int j = i + 2; double k = 20; String str = "summer"; }
局部变量表在编译期就可以确定,即局部变量表的槽位个数是已经确定的
上述代码对应的局部变量表如下
简单分析一下,method 方法是一个非静态的方法,所以在 method 栈帧对应的局部变量表槽位为 0 的地方存放了 this 指针,形式参数 i、参数 d 都有确定的值,i 占据槽位下标为 1 的位置,由于 d 是 double 类型的数据,需要占据两个槽位,所以 slot2、slot3 被 d 占用,int 类型的变量 j 也需要占据一个槽位,slot4 就分配给了 j 变量,类似的 double 类型的 k 也需要占用两个槽位,slot5、slot6 就分配给了 k,字符串是引用类型,占据一个槽位,所以 str 就在 slot7 的位置,通过 jclasslib 反解析也可以进行验证
对应字节码指令解析如下
// 1、将局部变量表 slot1 中的数据(形式参数 i 的值)压入操作数栈的栈顶位置 0 iload_1 // 2、将常数 2 压入操作数栈的栈顶位置(此时步骤 1 对应的操作数就在次栈顶了) 1 iconst_2 // 3、将操作数栈的栈顶和次栈顶两个操作数弹出操作数栈,然后进行加法操作,操作完成之后将结果重新压入操作数栈的栈顶位置(此时栈顶的操作数为 i + 2 的值) 2 iadd // 4、将操作数栈栈顶的元素弹出操作数栈,然后存入局部变量表的 slot4 位置(j 变量对应的槽位),注意此时操作数栈中已经没有任何操作数了 3 istore 4 // 从常量池中将 double 类型的常数 20.0 压入操作数栈栈顶的位置 5 ldc2_w #2 <20.0> // 5、将局部变量表 slot2 的 double 类型的操作数压入操作数栈栈顶位置(此时步骤 4 对应的操作数就在次栈顶了) 8 dload_2 // 将操作数栈的栈顶和次栈顶的两个操作数弹出操作数栈,进行加法操作,操作完成之后将结果重新压入操作数栈的栈顶位置(此时栈顶的操作数为 20.0 + d 的值) 9 dadd // 将操作数栈栈顶的元素弹出操作数栈,然后存入局部变量表的 slot5 位置(k 变量对应的槽位),注意此时操作数栈中已经没有任何操作数了 10 dstore 5 // 从常量池中将 String 类型的常数 summer 压入操作数栈栈顶的位置 12 ldc #4 <summer> // 将操作数栈栈顶的元素弹出操作数栈,然后存入局部变量表的 slot7 位置(str 变量对应的槽位),注意此时操作数栈中已经没有任何操作数了 14 astore 7 // 返回结果(此时操作数栈无任何操作数) 16 return
特别需要注意的是,局部变量表的槽位大小(个数)虽然在编译期就已经确定,但是为了节省内存空间,会出现槽位复用的情况
不妨看如下一段代码
public void method(int i) { { int j = 20; } int k = 100; }
上述代码的局部变量表有几个槽位呢?
可能大多数人会觉得会有 4 个槽位(理由如下: method 是非静态方法,slot0 存放 this 指针,int 类型的局部变量 i、j、k 均占用一个槽位)
但是通过反解析得到的结果是这个样子的
实际上只有 3 个槽位,j 变量并不占用槽位,为什么呢?
因为变量 j 出了大括号的作用域之后就失效了,当进行 int k = 100;时,为了节省内存空间,就没有必要再为 k 分配一个槽位了,变量 k 就直接复用变量 j 的槽位即可
标签:__,操作数,01,压入,int,局部变量,指令,类型 From: https://www.cnblogs.com/xiaomaomao/p/16926213.html