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虚拟机的描述,下面我根据上图用自己的话总结一下:

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

返回地址

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

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

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

相关推荐
喵手19 分钟前
如何利用Java的Stream API提高代码的简洁度和效率?
java·后端·java ee
掘金码甲哥25 分钟前
全网最全的跨域资源共享CORS方案分析
后端
m0_4805026433 分钟前
Rust 入门 生命周期-next2 (十九)
开发语言·后端·rust
张醒言39 分钟前
Protocol Buffers 中 optional 关键字的发展史
后端·rpc·protobuf
鹿鹿的布丁1 小时前
通过Lua脚本多个网关循环外呼
后端
墨子白1 小时前
application.yml 文件必须配置哇
后端
xcya1 小时前
Java ReentrantLock 核心用法
后端
用户466537015051 小时前
如何在 IntelliJ IDEA 中可视化压缩提交到生产分支
后端·github
小楓12011 小时前
MySQL數據庫開發教學(一) 基本架構
数据库·后端·mysql
天天摸鱼的java工程师1 小时前
Java 解析 JSON 文件:八年老开发的实战总结(从业务到代码)
java·后端·面试