jvm(四)虚拟机栈

内容

  • 概述
  • 运行时栈帧结构
  • 局部变量表
  • 操作数栈
  • 操作数栈字节码指令执行分析
  • 栈顶缓存技术
  • 动态链接
  • 方法返回地址
  • 一些附加信息

概述

  • 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应这一次次的java方法调用。
  • java虚拟机栈的生命周期跟线程一致。
  • 栈是运行时的单位,堆是存储的单位。
    栈解决程序的运行问题,也就是程序如何执行,或者如何处理数据;堆解决的是数据存储的问题,即数据怎么放,放在哪里。
  • 栈时一种快速有效的分配存储方式,访问速度仅次于程序计数器。
  • 对于栈来说,没有垃圾回收操作。
  • java虚拟机栈允许java栈的大小可以是动态的或者固定的。
    (1)固定大小的虚拟机栈:每个线程的java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机会抛出一个StackOverflowError异常
    (2)动态扩展:在创建新的线程时没有足够的内存去创建对应的虚拟机栈,java虚拟机就会抛出OutOfMemoryError异常
  • 设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
/**
 * 演示栈中的异常:StackOverflowError
 *
 *  默认情况下:count : 11420
 *  设置栈的大小: -Xss256k : count : 2465
 */
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
            count++;
            main(args);
    }
}

运行时栈帧结构

栈帧概念表.png
  • 栈中存储什么?
    (1)每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在。
    (2)线程上正在执行的每个方法都各自对应一个栈帧。
    (3)栈帧是一个内存区块,是一个数据集,维系着方法执行过程中各种数据信息。
    (4)栈只有压栈和出栈两个操作,在一条活动线程中,一个时间点上,只会有一个活动的栈帧,成为当前栈帧,对应的方法为当前方法,定义这个方法的类是当前类。
    (5)执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 栈运行原理
    (1)不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧中引用另外一个线程的栈帧
    (2)java方法有两种返回函数的方式:1.使用return指令,正常的函数返回 2.抛出异常。这两种方式都会导致栈帧被弹出
  • 栈帧内部结构:局部变量表 、操作数栈、动态链接(指向运行时常量池的方法应用)、方法返回地址(方法正常退出或者异常退出的定义)、一些附加信息

局部变量表

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,数据类型包含:基本数据类型、对象引用、returnAddress类型(返回值)
  • 局部变量表是建立在线程上的栈上,是线程私有数据,所以不存在数据安全问题
  • 局部变量表的容量大小在编译期间就确定了,并保存在方法的Code属性maximum local variables数据项中,在方法运行期间是不会改变的.
public class Test {
    public void show(int num){
        int age = 18;
    }
}

字节码文件

字节码文件
  • 方法嵌套调用次数由栈的大小决定。栈越大,方法嵌套调用次数越多,如果一个函数的参数和局部变量越多的话,栈帧就越大,函数调用就会占用更多的栈空间,导致嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
  • Slot的理解
    (1)局部变量表最基本的存储单元是slot(变量槽)
    (2)在局部变量表里,32位内的数据类型只占用一个slot,64位类型占用两个slot(long和double)
    (3)jvm会给局部变量表中每一slot分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
    (4)当一个实例方法被调用的时候,它的方法参数和方法内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
    (5)如果需要访问局部变量表中的一个64位的局部变量值时,只需要前一个索引即可(比如访问long或double)
    (6)如果当前帧是由构造方法或者实例方法创建的,该对象的引用this将会存放在index为0的slot处,其余参数按照参数表顺序继续排列
    (7)局部变量表中的变量是重要的垃圾回收根节点之一,只要被局部变量表中直接或间接引用的对象都不会被回收

操作数栈

  • 在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈/出栈
  • 操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
  • 操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行时,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的Code属性中,为stack的值


    栈深度
  • 操作数栈是通过入栈和出栈操作来完成一次数据访问
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈,并更新程序计数器中下一条需要执行的字节码指令。
  • java虚拟机的解释引擎是基于操作数栈的执行引擎。

操作数栈字节码指令执行分析

public class Test {
    public void add(){
        //byte、short、char、boolean:都以int类型保存
        byte i = 15;
        int j = 8;
        int k = i + j;
    }
}
字节码指令

1.程序计数器保存指定地址0,执行0地址的指令,bipush 15表示将15push进操作数栈,此时程序计数器存储下一条指令地址为2,此时局部变量表只有一个this的变量,


0

2.执行指令地址2,istore_1表示将15从操作数栈弹出,保存15到局部变量表,程序计数器保存下一条指令地址为3


2

3.执行指令地址3,bipush 8表示将8push进操作数栈,此时程序计数器存储下一条指令地址为5,
3.png

4.执行指令地址5,istore_2表示将8从操作数栈弹出,保存8到局部变量表,程序计数器保存下一条指令地址为6
5.png

5.执行指令地址6,将局部变量表中位置为1的值,push到操作数栈中,程序计数器保存下一条指令地址为7


6.png

6.执行指令地址7,将局部变量表中位置为2的值,push到操作数栈中,程序计数器保存下一条指令地址为8
7.png

7.执行指令地址8,iadd表示执行引擎将15和8出栈,并相加,将计算结果push到操作数栈,程序计数器存储下一个指令地址为9
8.png

8.执行指令地址9,istore_3表示将23从操作数栈弹出,保存23到局部变量表,程序计数器保存下一条指令地址为10
9.png

9.执行指令地址10,函数没有定义返回值,通过return结束函数。

栈顶缓存技术

基于栈式架构的虚拟机使用零地址指令会更加紧凑,意味着更多的指令分配次数和内存读写次数,由于操作数栈是存储在内存中的,频繁执行内存读写会影响执行速度。为了解决这个问题,Hospot设计者提出栈顶缓存技术,将栈顶元素全部缓存在物理cpu的寄存器中,来降低对内存的读写次数,来提升执行引擎的执行效率。

动态链接

*每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。目的时为了支持当前方法的代码能够实现动态链接,比如invokedynamic指令

  • 在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。


    动态链接

    部分字节码文件

方法返回地址

  • 存放调用该方法的程序计数器的值,让执行引擎执行下一步操作
  • 方法正常退出的话,程序计数器存储的指令地址作为方法返回地址,如果方法异常退出,返回地址要通过异常表来确定,栈帧一般不会保存这部分信息。

一些附加信息

  • 栈帧中允许携带与java虚拟机实现相关的一些附加信息,例如:对程序调试提供支持的信息。栈帧中不一定都有这部分数据。

你可能感兴趣的:(jvm(四)虚拟机栈)