前言
回想起当初自己刚学字节码指令时,看的文章资料倒是不少,但是几乎都差不多,翻来覆去说的都是差不多一样的东西,自己有许多疑问无法理解,这些疑问也没办法在网上搜(这让我深刻体会到了上学时老师的重要性(#°Д°)),那感觉太痛苦了。所以在此记录一下自己当时的疑问,希望对有缘看到这篇文章的小伙伴有所帮助。
1、局部变量表中变量个数问题
每个栈帧中的局部变量表的最大容量是在编译时期就被确定的,除了局部变量存储在局部变量表中,方法的参数也会存在局部变量表中。如果方法不是静态方法,那么方法所在类的对象引用也会存储在局部变量表中,并且是存在第一个槽中,名称是this
。在this后面存的是方法的参数,在方法参数后面才是存的方法里的局部变量。
槽:即slot,局部变量表的容量以槽为最小单位,一个槽可以容纳一个 32 位的数据类型(比如int)。
例如如下方法:
arduino
public class Test {
int add(int a,int b){
int value = a + b;
return value;
}
}
该方法栈帧的局部变量表最大容量为4,里面有4个变量:
如果该方法是静态方法,那么就没有this,局部变量表的最大容量为3。
另外局部变量表并不一定是一直都是存满的。作用域为整个方法域的局部变量是一直存于局部变量表中,但是作用域为方法中某一个代码块的变量(为便于理解这里称之为临时变量)是只有使用时才会存入局部变量表,并且离开临时变量的作用域后会从局部变量表中删除该临时变量,所以下一个临时变量也会被存于该临时变量呆过的槽中。
例如:
arduino
public class Test {
int add(int a,int b,boolean boo){
int value = a + b;
{
int c = 8;
}
if (boo){
int d = value;
}
return value;
}
}
思考一下上面的文件编译后,局部变量表最大容量是多少? 局部变量有this、a、b、boo、value、c、d共7个,最大容量是7?
用javap命令看一下,打印如下:
由上图可知,临时变量c和d都可存到第六个槽中,所以局部变量表最大容量为6即可。
2、局部变量表中变量的槽号问题
看如下例子:
arduino
public class Test {
long add(int a,long b){
long value = a + b;
return value;
}
}
javap打印如下:
这个例子的局部变量有this、a、b、value四个,为什么局部变量表最大容量比变量个数还多?为什么value的槽号是4(也就是第5个槽,因为槽号从0开始),槽号3去哪了?
因为b和value类型是long,上一小节说了,一个槽可以容纳一个 32 位的数据类型,而long是64位,所以需要占用两个槽的位置,如下图:
所以局部变量表最大容量是6,value存在第5、6两个槽中。除了long类型占两个槽
之外,double类型也占两个槽
。
3、跳转指令和ASM的Label理解
1. if跳转
ini
public class Test {
public void jump(int a) {
if (a == 18) {
a = 10;
} else {
a = 20;
}
}
}
上面代码是一个简单的if判断,来分别看一下javap的字节码指令、ASM的字节码指令和ASM Core API之间的关联:
可以看到,将局部变量a以及常量18压入操作栈后,执行了if_icmpne
指令,在javap的字节码中,if_icmpne后面的操作数是12,表示如果满足条件,跳转至指令12(对应于代码中else块中的语句),12之前的指令不再执行;如果不满足条件,则指令继续按顺序往下执行(对应于代码中if块中的语句),执行完if中的语句后,到指令9,会跳转至指令15,结束方法。
而在ASM中,由于是人为编写字节码文件,所以很难提前得知后面指令码的序号(比如一个文档的目录都是在文档写完后才创建,无法提前创建目录,因为只有文档写完后才能知道每个章节在第几页),也就无法像javap字节码中那样直接跳转至指令12,但是我们又需要知道要跳转到哪里,所以就引入了Label的概念。上图中IF_ICMPNE L0
就表示跳转至L0处,L0就是一个Label。
在编写ASM代码时,需要用到Label的时候就new一个出来,然后在后面要跳转到的地方调用visitLabel
方法插入label,这样就会跳转到label这里,继续往下执行后面的指令。注意Label不是jvm中的指令码,而是ASM中为了实现跳转功能而定义的,jvm中没有label这个东西。
2. switch跳转
switch跳转原理和if跳转一样,都是用Label,不过多赘述,这里就看一下如何写ASM代码。
arduino
public class Test {
public void jump(int a) {
String str;
switch (a){
case 0:
str = "0";
break;
case 1:
case 2:
str = "正数";
break;
default:
str = "负数";
break;
}
}
}
很简单的一个switch,javap看下字节码指令:
可以看到switch的字节码指令是tableswitch
。对应ASM代码如下:
一眼就能看到visitTableSwitchInsn
方法,源码中方法签名如下:
arduino
public void visitTableSwitchInsn(final int min, final int max, final Label dflt, final Label... labels) {
...
}
解释一下参数含义:
min、max
:switch的case的两个边界值,对应本例就是0和2
dflt
:default要跳转到的Label
labels
:一个Label类型的可变参数,对应每个case要跳转到的Label
这里可能有个疑惑,为什么可以直接设置case的两个边界值?它怎么知道我中间有多少个case?如果我的case是0,6,8三个,该怎么写?
实际上在字节码文件中,switch是分为两种的,一种就是本例中这种case值为连续的,还有一种则是case值不连续的。case值不连续的switch字节码指令是lookupswitch
。使用了visitTableSwitchInsn方法就说明该switch是连续的,所以只需传入两个边界值即可。
来看下case值不连续的switch:
arduino
public class Test {
public void jump(int a) {
String str;
switch (a){
case 0:
str = "0";
break;
case 6:
case 8:
str = "正数";
break;
default:
str = "负数";
break;
}
}
}
javap后:
可以看到指令变成了lookupswitch。
在ASM中lookupswitch对应visitLookupSwitchInsn
方法,方法签名为:
arduino
public void visitLookupSwitchInsn(final Label dflt, final int[] keys, final Label[] labels) {
...
}
dflt
:default要跳转到的Label
keys
:所有case值的数组,对应本例就是[0,6,8]
labels
:所有case值对应跳转到的label数组
需要注意的是,dflt不能传null,即使你的java代码switch没有写default分支,在编译成字节码文件的时候,jvm也会自动给加上default,default会直接跳转至return指令,所以dflt也要传入一个label。示例如下:
参考文章: