Jvm字节码指令答疑

前言

回想起当初自己刚学字节码指令时,看的文章资料倒是不少,但是几乎都差不多,翻来覆去说的都是差不多一样的东西,自己有许多疑问无法理解,这些疑问也没办法在网上搜(这让我深刻体会到了上学时老师的重要性(#°Д°)),那感觉太痛苦了。所以在此记录一下自己当时的疑问,希望对有缘看到这篇文章的小伙伴有所帮助。

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。示例如下:

参考文章:

Java字节码指令详解,1 万字20 张图带你彻底掌握字节码指令 | 二哥的Java进阶之路

JVM 字节码 对照表

相关推荐
魔道不误砍柴功2 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2342 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨2 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟4 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity5 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天5 小时前
java的threadlocal为何内存泄漏
java
caridle5 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^5 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋35 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花5 小时前
【JAVA基础】Java集合基础
java·开发语言·windows