Java虚拟机栈--超详解

微信公众号/CSDN/知乎同名:Java传家宝

虚拟机栈超详解

​ Java虚拟机栈作为Java虚拟机运行时数据区的一部分,是线程私有的,描述了Java方法的内存模型。结构如图所

image-20231023191243969

image-20231023191243969

当方法调用时,会在虚拟机栈中创建一个栈帧,在栈帧中保存方法的局部变量表,操作数栈,动态连接和返回地址。以下对其详解

局部变量表

​ 在局部变量表中,分为一个个Slot,方法执行时先bipush指令将变量放在操作数栈中,通过istore指令存储在slot中。存储了方法在编译期可知的各种基本数据类型对象引用(非对象本身,相当于一个指针指向Java堆中的对象实例)。

image-20231023194820907

image-20231023194820907

操作数栈

​ 操作数栈存储当前时刻的操作数,在方法运行会进行一系列的入栈出栈操作。这部分比较抽象,结合一个例子来看,比如调用如下方法

public static void main(String[] args){
    int i = 100;
    int j = 200;
    int j += i;
}

​ 结合上文局部变量表的理解,操作数栈内容和局部变量表变化如图

image-20231023201413337

image-20231023201413337

先解释其中的一些字节码指令

字节码指令效果
bipush将整型变量推入操作数栈 栈顶
istore_n将操作数栈栈顶整型变量出栈并保存在局部变量表第n个slot中
iload_n将局部变量表第n个slot整型变量复制到操作数栈栈顶中
iadd_n将操作数栈头两个栈元素出栈并做整型加法,结果入栈

由图示应该能理解操作数栈的作用了。首先,操作数栈是空的,当方法开始执行,会进行不断入栈出栈过程,实现一系列的方法操作。

动态连接

首先,在每个栈帧中包含一个指向运行时常量池中该栈帧所属方法的引用,用于支持动态连接。其次动态连接指的是只有在运行期间才能确定方法调用版本的方法,在方法调用时,将常量池中的符号引用替换为直接引用的过程。这句话包含的信息较多,我们挨个进行解析。

常量池

当我们的程序通过javac编译为Class字节码后,常量池指的就是Class文件中保存的字面量符号引用。属于编译期的概念,Class文件记录的部分数据结构如图

image-20231023205333873

image-20231023205333873

字面量接近于常量的概念,比如final的常量,文本字符串等。

符号引用相当于是一种类似标志的概念?,此时并不能通过它获得真正的内存入口,主要包括有:

  • 类和接口的全限定名

  • 字段和方法的名称和描述符

image-20231023211612173

image-20231023211612173

运行时常量池

首先,它属于方法区的一部分,在类加载之后,常量池的内容将会存放至运行时常量池中,此外,还存放了将符号引用解析后的直接引用。(方法区和类加载在方法区超详解细讲)

image-20231023211842892

image-20231023211842892

方法调用

​ 方法调用即字面意思,就是方法的调用,但是如何选定正确的版本是一个问题。对于在程序写好,编译时就能够确定版本的方法,调用时称为解析调用,反之称为分派调用,分派调用又分为静态分派动态分派。下文依次解析

image-20231023212827398

image-20231023212827398

解析调用

​ 解析调用指的是,在类加载的解析阶段,对于在程序写好,编译时就能够确定版本的方法,会将其符号引用转化为直接引用。一般为包括静态方法、私有方法、实例构造器和父类方法,他们也被称为非虚方法(直接就确定类型了,一点都不‘虚’)。

​ 在字节码层面讲,能够被invokestaticinvokespecial调用的方法都可以在解析阶段确定唯一的调用版本。

字节码指令效果
invokestatic调用静态方法
invokespecial调用实例构造器、私有方法和父类方法
invokevirtual调用所有的虚方法
分派调用

​ 分派调用就指的时,在程序写好,编译时不能够确定版本的方法。又分为静态分派和动态分派。

静态分派

​ 依赖于静态类型来定位方法执行版本的称为静态分派。最常见的就是方法的重载,发生在编译阶段。比如如下代码

public class StaticDispatch {
    static abstract class Human{}
    static class Man extends Human{}
    static class Woman extends Human{}
    public void sayHello(Man man){
        System.out.println("Hello, man");
    }
    public void sayHello(Woman woman){
        System.out.println("Hello, woman");
    }
    public void sayHello(Human human){
        System.out.println("Hello, human");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayHello(man);
        staticDispatch.sayHello(woman);
    }
}

对于上述代码,最终输出

Hello, human
Hello, human

即,根据静态类型调用对应的方法。所谓静态类型就如下代码中,man的静态类型就是Human,实际类型是Man。最终方法调用的是重载后Human对应得版本。

Human man = new Man();
动态分派

​ 动态分派就是通过实际类型确定方法调用的版本。常见的就是方法的重写。比如如下代码

class DynamicDispatch {
    static interface Human{
        public void sayHello();
    }
    static class Man implements Human{

        @Override
        public void sayHello() {
            System.out.println("Hello, Man");
        }
    }
    static class Woman implements Human{
        @Override
        public void sayHello() {
            System.out.println("Hello, Woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
    }
}

最终运行结果为根据实际类型调用的对应的方法。

Hello, Man
Hello, Woman

如何实现动态分派的呢?

我们主要关注invokevirtual,它主要分为三步骤:

  • 首先匹配对象的实际类型

  • 根据实际类型找到在虚方法表中与常量池中的描述符和名称都匹配的方法

  • 匹配成功后根据访问权限校验
    • 通过就返回方法的直接引用(栈帧从而拿到了该方法的直接引用)

    • 不通过就抛出异常

虚方法表指的是存放了各个方法的实际入口地址:意思就是如果子类未重写父类方法,那么虚方法表内指向的就是父类方法地址,如果重写了,就指向子类方法地址。

image-20231023225813435

image-20231023225813435

之前动态分派的实现是参考JVM虚拟机的描述,下面我根据上图用自己的话总结一下:

首先,会在操作数栈中创建方法所有者,根据方法所有者的实际类型虚方法表中找到与常量池中的符号引用一致的方法的直接引用,然后返回给栈帧保存在动态连接中。

返回地址

​ 方法退出一般分为两种方式:

  • 方法正常退出,此时返回地址可以为方法调用者的程序计数器值。(有点像方法执行前的程序计数器指向的字节码地址)

  • 方法异常退出时,此时返回地址不保存在栈帧中,而是通过异常处理表来决定的。

方法退出过程其实就是栈帧出栈的过程,如果有返回值,就将返回值压入调用方法者的操作数栈的栈顶,通过调整程序计数器的值,以指向方法调用后的下一条字节码指令。

image-20231024100227931

image-20231024100227931

觉得文章有用可关注我的同名微信公众号:Java传家宝

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值