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 字节码 对照表

相关推荐
东阳马生架构4 分钟前
商品中心—1.B端建品和C端缓存的技术文档
java
Chan167 分钟前
【 SpringCloud | 微服务 MQ基础 】
java·spring·spring cloud·微服务·云原生·rabbitmq
LucianaiB9 分钟前
如何做好一份优秀的技术文档:专业指南与最佳实践
android·java·数据库
面朝大海,春不暖,花不开33 分钟前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
得过且过的勇者y34 分钟前
Java安全点safepoint
java
夜晚回家1 小时前
「Java基本语法」代码格式与注释规范
java·开发语言
斯普信云原生组1 小时前
Docker构建自定义的镜像
java·spring cloud·docker
wangjinjin1801 小时前
使用 IntelliJ IDEA 安装通义灵码(TONGYI Lingma)插件,进行后端 Java Spring Boot 项目的用户用例生成及常见问题处理
java·spring boot·intellij-idea
wtg44521 小时前
使用 Rest-Assured 和 TestNG 进行购物车功能的 API 自动化测试
java
白宇横流学长2 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端