0
点赞
收藏
分享

微信扫一扫

JVM方法的在虚拟机栈执行过程以及java反汇编

心如止水_c736 2022-03-30 阅读 15
java

 

目录

反汇编

1.什么是slot?

2.Java字节码指令格式

3.iconst_

局部变量表的第一个变量

栈中可能出现的异常

栈运行原理


如下图:

这篇我们将要通过反汇编来仔细了解代码在JVM内部的执行过程以及相关的存储原理

反汇编

之前说到过:

局部变量表中的变量不可以直接使用,必须通过相关指令加载至操作数栈中作为操作数使用。

那么我们新建一个test.java文件,里面写如下代码:

int a = 1 + 2;
int b = a + 3;

 接着在cmd中运行javap -c -v test (先javac test.java)便会得到如下结果:

 这些指令的意思是:

iconst_1 //把整数 1 压入操作数栈
iconst_2 //把整数 2 压入操作数栈
iadd //栈顶的两个数出栈后相加,结果入栈;实际上前三步会被编译器优化为:iconst_3
istore_1 //把栈顶的内容放入局部变量表中索引为 1 的 slot 中,也就是 a 对应的空间中
iload_1 // 把局部变量表索引为 1 的 slot 中存放的变量值(3)加载至操作数栈
iconst_3 
iadd //栈顶的两个数出栈后相加,结果入栈
istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中,也就是 b 对应的空间中
return // 方法返回指令,回到调用点

注意:

局部变量表以及操作数栈的容量的最大值在编译时就已经确定了,运行时不会改变。而且局部变量表的空间可以复用。

1.什么是slot?

slot 是局部变量表中的空间单位,虚拟机规范中有规定,对于 32 位之内的数据,用一个 slot 来存放,如 int,short,float 等;对于 64 位的数据用连续的两个 slot 来存放,如 long,double 等。引用类型的变量 JVM 并没有规定其长度,它可能是 32 位,也有可能是 64 位的,所以既有可能占一个 slot,也有可能占两个 slot。

2.Java字节码指令格式

指令格式:

Java的指令以字节为单位,也就是一个指令就是一个字节。比如iconst_1就是一条指令,它占一个字节。Java指令不超过256条。

指令的操作数分为两种:

  • 嵌入在指令中的,通常是指令字节后面的若干个字节。(嵌入式操作数)
  • 存放在操作数栈中的。(栈内操作数)

区别:嵌入式操作数是在编译时就已经确定的,运行时不会改变,它和指令一样存放于类文件方法表的 Code 属性中;栈内操作数是运行时确定的,即程序在执行过程中动态生成的。

举个栗子:

3.iconst_<i>

意思:把整数 i 放入操作数栈中。

        i 的范围是(m1, 0, 1, 2, 3, 4, 5),其中 m1 代表的是 -1。注意,这里的 i 并不是指令的操作数(即非嵌入式操作数,也非栈内操作数),如 iconst_1、iconst_2 和 iconst_3 都是由一个字节组成的字节码指令。我们可以把 i 可以看作是指令的 “隐含操作数”,即指令本身就蕴含了操作数。

        如果整数 i 超过 [-1, 5] 这个范围,就不能用 iconst_<i> 表示了,因为仅一个字节的字节码指令不可能蕴含所有的整数。此时就需要 bipush 这条指令了,这条指令有一个嵌入式操作数,由一个字节组成,用来表示要放入栈顶的那个整数,该整数放入栈顶时通过扩展符号位变为 32 位的整型。但是一个字节也表示不了所有的整数,如果整数值超过一个字节所能表示的范围,就只能通过 ldc 这条指令了,这条指令带有一个字节的嵌入式操作数,它代表的是一个指向运行时常量池中 Constant_Integer_info 类型常量的索引,通过索引的方式引用运行时常量池中的整数,再大的整数也不怕了。

将原程序的值更改一下再次反编译(记得重新编译生成新的class文件)

 得到如下结果:

可以发现第一条指令变成了bipush

这次将数改的更大:

 

 

局部变量表的第一个变量

        对 JVM 而言,静态方法和实例方法的本质区别在于是否需要和具体对象关联:静态方法可以通过类名来调用,它不需要和具体对象关联;而实例方法必须通过对象来进行调用,它需要和具体对象关联。

实例方法是如何和对象产生关联的呢?

        编译器在编译时会将方法接收者作为一个隐含参数传入该实例方法,这个参数在方法中有一个很熟悉的名字,叫做 “this”。之所以实例方法可以访问该类的实例变量和其它实例方法,正是因为它有 “this” 这个隐含参数。

class accessMember{
    private static int sa; //定义一个静态成员变量
    private int ia; //定义一个实例成员变量
    //下面定义一个静态方法
    static void statMethod(){
      int i = 0; //正确,可以有自己的局部变量
        sa = 10;//正确,静态方法可以使用静态变量
      otherStat(); //正确,可以调用静态方法
      ia = 20; //错误,不能使用实例变量
      insMethod(); //错误,不能调用实例方法
    }
    static void otherStat(){}
    //下面定义一个实例方法
    void insMethod(){
      int i = 0; //正确,可以有自己的局部变量
      sa = 15; //正确,可以使用静态变量
      ia = 30; //正确,可以使用实例变量
      statMethod(); //正确,可以调用静态方法
    }
}

        比如我们定义的方法void insMethod(),它是实例方法,因此会有一个指向具体对象的隐含参数 this,this 就存放在局部变量表的第一个位置,即存放在索引为 0 的 slot 中,又由于它的作用域从方法开始一直到方法结束,因此它在局部变量表中的位置不会被其他变量覆盖,从而使得我们在方法中定义的变量只能放在局部变量表后面的位置中。

        如果方法有参数(非隐含参数),那么参数会按顺序紧接着 this 存放在局部变量表中,由于参数作用域也是整个方法体,所以方法中定义的局部变量就只能放在参数后面了。总的来说局部变量表中变量的存放顺序为: this(如果是实例方法)=> 参数(如果有的话)=> 定义的局部变量(如果有的话)。

栈中可能出现的异常

固定:每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackoverflowError 异常。

动态扩展:在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

例如:

private static int count = 0;
public static void main(String[] args) {
    print();
}
public static void print(){
    System.out.println(count++);
    print();
}

 

 

栈运行原理

  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

单线程内的栈桢是相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。

        如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

        Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

举报

相关推荐

0 条评论