Java虚拟机栈--详解

微信公众号、知乎、掘金、博客园、CSDN同名Java传家宝

虚拟机栈超详解

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

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

局部变量表

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

操作数栈

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

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

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

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

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

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

动态连接

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

常量池

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

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

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

  • 类和接口的全限定名
  • 字段和方法的名称和描述符

运行时常量池

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

方法调用

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

解析调用

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

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

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

分派调用

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

静态分派

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

java 复制代码
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对应得版本。

ini 复制代码
Human man = new Man();
动态分派

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

typescript 复制代码
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,它主要分为三步骤:

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

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

  • 匹配成功后根据访问权限校验

    • 通过就返回方法的直接引用(栈帧从而拿到了该方法的直接引用)
    • 不通过就抛出异常

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

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

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

返回地址

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

  • 方法正常退出,此时返回地址 可以为方法调用者的程序计数器值。(有点像方法执行前的程序计数器指向的字节码地址)
  • 方法异常退出时,此时返回地址 不保存在栈帧中,而是通过异常处理表来决定的。

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

相关推荐
城沐小巷37 分钟前
外卖点餐系统小程序
前端·后端·微信小程序
uhakadotcom1 小时前
开发我们的第一个基于Rust的应用:端口扫描器
后端·安全·rust
凌虚(失业了求个工作)3 小时前
AI 声音:数字音频、语音识别、TTS 简介与使用示例
人工智能·后端·python·深度学习·语音识别
Achou.Wang3 小时前
go结构体匿名“继承“方法冲突时继承优先顺序
开发语言·后端·golang
DARLING Zero two♡3 小时前
当前就业形势下C++方向后端开发学习指南
开发语言·c++·后端·团队开发
豆 腐4 小时前
Spring Boot【三】
java·spring boot·后端
你邻座的怪同学4 小时前
SpringBoot集成ESAPI
java·spring boot·后端
哎呦没4 小时前
宠物领养平台构建:SpringBoot技术路线图
spring boot·后端·宠物
Tdm_8887 小时前
C# 反射详解
java·开发语言·后端·c#·asp.net
桃园码工10 小时前
第二章:编写第一个 Go 程序 1.Hello World 程序 --Go 语言轻松入门
开发语言·后端·golang