04、JVM实战:运行时数据区之虚拟机栈

2.1 概述

2.1.1 栈和堆

栈管运行,堆管存储

2.1.2 java虚拟机栈是什么

java虚拟机栈是线程私有的,每个线程启动的时候都会创建一个虚拟机栈,对应着java方法的调用

2.1.3 作用

主管程序的运行,保存方法的局部变量(基本数据类型可以直接存储,引用数据类型存储引用地址),部分结果,参与方法的调用的返回

2.1.4 栈的特点

1、 栈是一种快速有效的存储方式,仅次于PCregister;
2、 栈不存在垃圾回收问题,存在OOM;
3、 jvm对栈的操作只有两种;

1、 随着方法的执行入栈;
2、 方法执行完毕出栈;

2.2 常见问题

2.2.1 虚拟机栈可能出现的异常

jvm规范是允许虚拟机栈的大小是固定的或者动态的

1、 固定如果线程创建的时候请求分配的栈容量超过了java虚拟机栈允许的最大容量,抛出StackOverflowError异常;
2、 动态如果在动态扩展的时候无法获取到足够的内存,或者线程申请虚拟机栈的时候没有足够的内存创建就会抛出OutOfMemoryError异常OOM;

2.2.2 设置栈的大小

-Xss 设置栈的大小
*

2.3 栈的存储单位

栈的存储单元是栈帧
java一个方法与一个栈帧相对应
栈帧是一个内存区块,维系着方法执行过程中的各种数据信息

2.3.1 栈的原理

1、 jvm对栈的操作只有两种压栈和出栈遵循FILO/LIFO(先入后出/后入先出)原则;
2、 同一线程同一时间点只有一个栈帧是有效的,也就是位于栈顶的栈帧,称为当前栈帧,与当前栈帧对应的方法就是当前方法,与当前方法对应的就是当前类
3、 执行引擎执行的指令也是当前栈帧进行操作,PCRegister存储的也是当前栈帧的指令地址;
4、 如果在当前方法中调用了新的方法,那么新的栈帧就被创建出来,成为新的当前栈帧;
5、 不同线程中包含的栈帧是不允许存在相互引用的,不可能在一个栈帧中引用另外一个线程的栈帧;
6、 方法有两种返回返回的方式,不管是哪种返回都会导致栈帧弹出;

1、 方法执行完毕,正常返回;
2、 抛出异常返回;

2.3.2 栈祯的存储结构

1、 局部变量表LocalVariables
2、 操作数栈OperandStack
3、 动态链接DynamicLinking;
4、 方法返回地址ReturnAddress;
5、 附加信息;

2.3.2.1 局部变量表

1、 局部变量表是一个数字数组,存储方法参数和方法体内的局部变量,包括8种基本数据类型,对象引用和returnAddress类型(返回类型);
2、 局部变量表线程安全,因为存放在栈帧中,是线程私有的;
3、 局部变量表的大小是编译期就确定的,运行期不会再改变;
4、 方法嵌套调用的次数由栈的大小决定,也就是栈内栈帧的多少由栈的大小决定;
5、 当方法执行完毕后,栈帧销毁,局部变量表也随之销毁;

例子:

package com.zy.study04;

/**
 * @Author: Zy
 * @Date: 2021/7/28 11:36
 */
public class StackFrameTest {
   
     
    public static void main(String[] args) throws CloneNotSupportedException {
   
     
        StackFrameTest test = new StackFrameTest();
        test.clone();
        int num = 1;
    }
}

使用jclasslib查看局部变量表
*
可以看到局部变量表中存储了main方法的两个变量和一个形参,所以局部变量表的大小为3

变量槽slot,局部变量表的基本单位
局部变量表中32位以内占用一个slot(包括引用数据类型),64位的占用两个slot(long和double)

局部变量表与类变量的对比:

1、 变量的分类;

1、 变量按数据类型分类:;

1.  基础数据类型
2.  引用数据类型

2、 变量按作用域分类:;

1.  成员变量
    
    1.  类变量即静态变量
    2.  实例变量
2.  局部变量

2、 对比局部变量和类变量:;

1、 类变量经历linking/prepare阶段进行默认赋值,initaizal阶段进行初始化赋值(静态代码块)局部变量不会默认赋值,在使用前必须显式赋值;

补充说明:
局部变量表的调优是性能调优的重要部分,因为局部变量表占据栈帧的很大一份空间,而且被局部变量表直接引用或者间接引用的对象都不会被回收,涉及到垃圾回收算法的垃圾回收根节点

2.3.2.2 操作数栈(Operand Stack)

操作数栈是基于的数组实现的栈结构(后入先出)
操作数栈: 在方法执行中,根据字节码指令,往操作数栈中写入数据/提取数据,也就是入栈/出栈操作(执行引擎).

1、 操作数栈主要用于保存计算过程的中间结果,同时也作为计算过程中变量的临时存储空间
2、 操作数栈在就是jvm执行引擎的一个工作区,当刚开始执行一个方法的时候,创建了一个栈帧,这个时候操作数栈是空的(数组深度已经有了)
3、 与局部变量表相同的是,操作数栈的深度在编译期也已经确定好了,运行时不能修改操作数栈的深度方法的Code属性的Max_stack属性;
4、 同样的32bit的类型占用一个栈单位,64位占用两个栈单位;
5、 操作数栈是栈,只能由入栈/出栈操作,因此,即使操作数栈本质上是一个数组,但是依然不能使用索引来访问操作数栈中的数据
6、 如果当前被调用的方法带有返回值时,返回值会被压入当前栈帧的操作数栈,并更新pc寄存器的下一条指令
7、 操作数栈的元素的数据类型必须与字节码指令的序列严格匹配,这个由编译器在编译期进行验证,并且在类加载阶段的linking/检验阶段再次验证;
8、 jvm的执行引擎是基于栈的此处的栈就是操作数栈

2.3.2.3 动态链接(Dynamic Linking)

1、 每个栈帧内部都包含一个指向运行时常量池中当前的方法的引用,包含整个引用的目的就是为了实现动态链接;
2、 在java源代码编译成字节码的时候,所有的变量和方法引用都会作为符号引用保存在class的常量池中,动态链接的作用就是将这些符号引用转换为对方法的直接引用

2.3.2.3.1 方法的调用

jvm中方法由符号引用转为直接引用与方法的绑定机制有关

1、 动态链接编译期无法确定的,只能在运行期才能将方法的符号引用转为直接引用,这样情况叫做动态链接;
2、 静态链接当一个字节码文件被装入jvm的时候,被调用的方法编译期可知,并且在运行期一直保持不变,这样情况下将方法的符号引用转换为直接引用的过程叫做静态链接;

方法的绑定机制:
绑定指的是方法/属性由符号引用转为直接引用的过程,不管什么时期的绑定都只会发生一次

1、 晚期绑定与动态链接对应;
2、 早期绑定与静态链接对应;

虚方法与非虚方法:

1、 虚方法就是动态链接/晚期绑定非虚方法就是静态链接/早期绑定;
2、 静态方法,final方法,私有方法,构造器方法,父类方法因为在编译期就能确定具体的调用版本,所以被称为非虚方法,其他方法称为虚方法;

方法调用的字节码指令:

1、 invokeStatic:调用静态方法;
2、 invokeSpecial:调用所有方法,私有方法,父类方法,即(除final方法外)的非虚方法;
3、 invokevirtual:调用所有虚方法(还包括final方法);
4、 invokeInterface:调用所有接口方法;
5、 invokeDynamic:动态调用jdk7新增指令;

1、 主要为了支持动态语言特性(java语言还是一种静态语言);
2、 例如lambda表达式就是动态调用的,jdk8才有了直接生成动态调用指令的方式;

虚方法的调用过程:
虚方法大多数情况都是因为方法的重写,方法重写的本质从方法调用的角度来看:

1、 操作数栈栈顶的第一个元素执行对象的类型,记作C;
2、 调用C的方法时,通过查询常量池中是否有类型符合,简单名称符合的方法,如果有进行权限校验,校验通过则返回此方法的引用;
3、 如果不符合,则按照继承关系查找C的父类,以此进行方法判断,如果符合,就返回方法的引用,否则一直执行;
4、 如果直到最后一个父类也没有合适/符合的方法,抛出异常AbstractMethodError;

由于上述循环查询父类的方式影响性能,为了提高性能,所以在类的方法区建立了一个 虚方法表
虚方法表建立后,就可以根据虚方法表获取要调用方法的引用,不用再递归获取.
虚方法表中,如果重写过的方法,指向的就是子类重写的方法的本质,如果没有重写方法,方法就还是指向父类的方法

2.3.2.4 方法返回地址 Return Address

存放该方法(栈帧)的pc寄存器的值,也就是下一条执行指令
方法退出有两种方式: 1. 正常退出 2. 未处理异常退出

1、 正常退出的时候,一般会回到方法被调用的位置,会把pc寄存器中的值存放到方法返回地址中.;
2、 异常退出的时候,一般通过异常表来确定方法返回地址,不会存放在栈桢中.;
3、 正常退出与异常退出的区别在于,异常退出不会给上层调用返回任何信息.;

2.3.2.5 一些附加信息

根据虚拟机可选,有些虚拟机存在,附件一些与程序调试相关的信息

2.3.3 栈顶缓存技术(Top Of Stack Cashing)

HotSpot JVM的设计者提出的 将栈顶元素全部缓存至物理cpu的寄存器中,减少对内存的读/写,加快执行效率

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: