① 概述

由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

优点:跨平台,指令集小,编译器容易实现。

缺点:性能下降,实现同样的功能需要更多的指令。

  • 是什么?

Java虚拟机栈,早期也叫Java栈。每个线程再创建时都会创建一个相对应的虚拟机栈,其内部保存着一个个栈帧,对应着一次次的Java方法调用。虚拟机栈是线程私有的。

  • 生命周期

与线程的生命周期相同。

  • 作用

管理Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

优点:

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。

Jvm直接对Java栈的操作只有两个:

  1. 每个方法执行,伴随着入栈。
  2. 执行结束后的出栈工作。

对于栈来说不存在垃圾回收问题。

栈中常见异常:

由于Java虚拟机允许Java栈的大小是动态的或者是固定不变的

如果采用固定大小的虚拟机栈,那每一个线程的虚拟机栈容量将再线程创建的时候独立选定。如果线程请求分配的栈容量超过了虚拟机栈允许的最大容量,则虚拟机会抛出一个StackOverflowError异常。

如果采用动态扩展的虚拟机栈,如果线程尝试扩展虚拟机栈的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那虚拟机会抛出OutOfMemoryError异常。

如何设置栈大小(IDEA中)

run -> Edit Configurations -> VM options:-Xss256k

单位有 k / m / g

② 栈的存储单位

栈中的数据都是以栈帧的格式存在的。

在栈中运行的每个方法都对应一个栈帧

栈帧是一个内存块,是一个数据集,这个数据集维系着方法执行过程中的所需数据信息。

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

栈帧的内部结构:

image-20220217114949043

  • 局部变量表。
  • 操作数栈。
  • 动态链接。
  • 方法返回地址。
  • 一些附加信息。
③ 局部变量表/本地变量表
  • 局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法内部的局部变量。这些数据类型包括各种基本数据类型、对象引用、以及returnAddress类型。
  • 由于栈为线程独享的,所以不存在线程安全问题。
  • 局部变量表所需的容量大小在编译期就已经确定下来了,并保存在方法的Code属性的maximum local variables数据项中。在方法的运行期间是不会改变局部变量表的大小的。

如何查看一个方法的局部变量表大小?

public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        int i = 100;
    }
}

编译后使用javap -v Main.class指令将字节码反汇编为汇编语言,其中标注了局部变量表大小。

在这里插入图片描述

查看局部变量表大小。

Slot局部变量表最基本的存储单元:

上面说过,局部变量表是一个int[],其特性和int[]相同,从0位置开始到length - 1

最基本的存储单元为Slot变量槽

其中:32位以内的类型只占用一个变量槽64位的类型占用两个变量槽。

  • byte,short,char,boolean,float在存储前被转换为int且占据一个Slot
  • long,double在存储前被转换为int且占据两个Slot

静态方法比非静态方法多存储一个this变量。

Slot重复利用:

栈帧中的局部变量表中的槽位是可以被重复利用的,如果一个局部变量过了其作用域,那么之后申请的新的局部变量就会复用之前的局部变量槽位。

//局部变量表大小为 3
public void test() {
    int a = 0;
    {
        int b = 0;
        b = a + 1;
    }
    //变量 c 使用的是局部变量 b 的槽位。
    int c = a + 1;
}
④ 操作数栈

主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

操作数栈就是JVM执行引擎的一个工作区域,当一个方法开始执行的时候,一个新的栈帧被随之创建,那么这个操作数栈也是空的。

操作数栈的表现为一个一维整型数组int[],那么当这个int[]被创建的时候,即使是空的,那么也是有长度的。

在这里插入图片描述

栈中的任何一个元素都可以是任意的Java数据类型。

  • 32bit数据占一个单位深度。
  • 62bit数据占两个单位深度。
⑤ 操作数栈的代码演示
//Java代码
public void test() {
    int a = 3;
    int b = 1;
    int c = a + b;
}


//字节码
0 iconst_3      //将3存入操作数栈中
1 istore_1      //将3从操作数栈中取出并放入局部变量表中索引为1的位置(因为0位置存放的是this)
2 iconst_1      //将1存入操作数栈
3 istore_2      //将1从操作数栈取出,存入局部变量表索引为2的位置
4 iload_1       //取出局部变量表索引为1的值,存到操作数栈中
5 iload_2       //取出局部变量表索引为2的值,存到操作数栈中
6 iadd          //相加并出栈  得到结果继续存入操作数栈  此时操作数栈只有一个值,即相加的结果
7 istore_3      //将相加结果存入到局部变量表索引为3的位置
8 return
⑥ 栈顶缓存技术

由于操作数是存储于内存中的,因此频繁地执行内存读/写操作必然会影响速度。

所以HotSpot JVM设计师们提出了栈顶缓存技术:

将栈顶元素全部缓存在物理CPU的寄存器中,一次降低读取次数,提升执行效率。

⑦ 动态链接

https://www.bilibili.com/video/BV1PJ411n7xZ?p=55

⑧ 方法的调用:解析与分配
  • 早期绑定:早期绑定指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法于所属的类型进行绑定。因此也就可以使用静态链接的方式将符号引用转换为直接引用。

  • 晚期绑定:如果被调用的方法在编译期无法确定,只能在程序运行期间根据实际的类型绑定相关的方法,这种绑定方式称为晚期绑定。

**非虚方法:**不涉及到多态

  1. 如果方法在编译期就确定了具体调用版本,并且这个版本在运行时不可变。这样的方法称为非虚方法。例如:抽象发放不是非虚方法,因为它会被所有它的实现类重写。
  2. 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法

字节码中的体现:

  • invokestatic:调用了静态方法、解析阶段确定唯一方法版本。
  • invokespecial:调用了<init>方法、私有或父类方法。解析阶段确定唯一版本。
  • invokevirtual:调用了虚方法。调用final也会出现此条字节码,但final方法不是虚方法。
  • invokeinterface:调用了接口方法。
  • invokedynamic:动态解析出需要调用的方法。
⑨ 方法返回地址

存放调用该方法的pc寄存器的值。

也就是将本方法的返回值压入调用方的操作数栈。

⑩ 一些附加信息

略。

⑩① 相关面试题

举例栈溢出的情况?

StackOverflowError:压栈超过了栈最大容量,或递归没有终止条件。

可以通过-Xss设置栈大小来解决,当然后者无法解决。

调整栈大小,就能保证不溢出了吗?

不能保证的,递归没有中止条件的时候不能解决栈溢出。

分配的栈内存越大越好吗?

肯定不是越大越好。对自己线程好,但是对别人就不好了。

垃圾回收涉及到栈空间吗?

不涉及。

方法中定义的局部变量是否线程安全?

具体问题具体分析。

如果方法中开启线程,公用方法中局部变量,则不安全。

如果没有开启线程,或开启外部线程,则是线程安全的。

Logo

长江两岸老火锅,共聚山城开发者!We Want You!

更多推荐