相关文章:
一、Java 虚拟机栈 (Java Virtual Machine Stacks)
-
与程序计数器一样,Java 虚拟机栈 (Java Virtual Machine Stacks) 也是线程私有的,它的生命周期与线程一样。虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧 (Stack Frame:栈帧是方法运行时的基础数据结构) 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
-
经常有人把 Java 内存区分为堆内存 (Heap) 和栈内存 (Stack),这种分发比较粗糙,Java 内存区域的划分实际上远比这复杂,这种划分方式的流行只能说明大多数程序员最关注、与对象内存分配关系最密切的内存区域是这两块。这里的栈,实际指的就是虚拟机栈,或者说是虚拟机栈中的局部变量表部分
-
局部变量表存放了编译器可知的各种基本数据类型 (boolean、char、byte、short、int、long、float、double)、对象引用 (reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针;也可能是指向一个代表对象的句柄或是其他与此对象相关的位置) 和 returnAddress 类型 (指向了一条字节码指令的地址)
-
其中 64 位长度的 long 和 double 类型的数据会占用 2 个局部变量空间 (Slot),其余的数据类型只会占用 1 个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小
-
在 Java 虚拟机规范中,对这个区域规定了两种异常状况:如果线程请求的栈深度大于虚拟机所允许的深度,则会抛出 StackOverflowError ;如果虚拟机栈可以动态扩展 (当前大部分的 Java 虚拟机都可以动态扩展,只不过 Java 虚拟机中也允许固定长度的虚拟机栈),如果扩展时无法申请到足够的内存,则会抛出 OutOfMemoryError 异常
二、局部变量表和操作数栈的关系
-
局部变量表
-
为操作数栈提供必要的数据支撑
-
包含了方法执行过程中的所有变量 --> 各种基本数据类型、对象引用、returnAddress 类型
-
-
操作数栈
- 在执行字节码指令过程中被使用,这种方式类似于原生 CPU 的寄存器,大部分字节码把时间花费在操作数栈的操作上,包括入栈、出栈、复制、交换、产生消费变量等操作
-
众所周知,栈是一个后进先出的数据结构,因此当前执行的方法在栈的顶部,每次方法调用时,一个新的栈帧会被创建并压栈到栈顶,当方法正常返回或抛出未捕获的异常时,栈帧就会出栈,除了栈帧的压栈和出栈,栈不能被直接操作
三、示例如下
-
ByteCodeSample.java
public class ByteCodeSample { public static int add(int a, int b) { int c = 0; // 第6行 c = a + b; // 第7行 return c; // 第8行 } }
-
ByteCodeSample.class
Classfile /C:/Users/XJ/Desktop/ByteCodeSample.class Last modified 2019-10-21; size 289 bytes MD5 checksum a966ee68afd5837b15a221d493eaa001 Compiled from "ByteCodeSample.java" public class com.xj.classloader.ByteCodeSample minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #3.#12 // java/lang/Object."<init>":()V #2 = Class #13 // com/xj/classloader/ByteCodeSample #3 = Class #14 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 add #9 = Utf8 (II)I #10 = Utf8 SourceFile #11 = Utf8 ByteCodeSample.java #12 = NameAndType #4:#5 // "<init>":()V #13 = Utf8 com/xj/classloader/ByteCodeSample #14 = Utf8 java/lang/Object { public com.xj.classloader.ByteCodeSample(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 public static int add(int, int); descriptor: (II)I flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=3, args_size=2 0: iconst_0 1: istore_2 2: iload_0 3: iload_1 4: iadd 5: istore_2 6: iload_2 7: ireturn LineNumberTable: line 6: 0 line 7: 2 line 8: 6 } SourceFile: "ByteCodeSample.java"
-
如上所示,我们创建了一个名为 ByteCodeSample 的类,然后使用 javap -verbose (用口语化的形式来描述 Class 文件) 指令对 ByteCodeSample.class 文件进行解析,得到对应的解析内容,这里我们重点研究 add() 方法
变量名 值 含义 descriptor (II)I 表示该该方法接收两个
int 类型的变量,并返回一个 int 类型flags ACC_PUBLIC 表示该方法用 public 进行修饰 ACC_STATIC 表示该方法用 static 进行修饰 Code stack=2 表示操作数栈的深度为 2 locals=3 表示局部变量表容量为 3 args_size=2 表示参数个数为 2 -
这里假设两个参数分别是 1 和 2,即执行了 add(1, 2)方法,我们将字节码指令转换为图形展示
-
如上所示,展示的是 add(1, 2) 方法的操作 (一个方法只会对应一个栈帧,这里是同一个栈帧的 7 次变化),我们可以清晰地看到:局部变量表的容量为 3 (即 locals);操作数栈的深度为 2 (即 stack);此外,load 代表入栈;store 代表出栈
-
iconst_0
- 将常量 0 压入操作数栈栈顶
-
istore_2
- 将常量 0 出栈,将其存入局部变量表中,作为第 2 个元素 (从第 0 个开始算)
-
iload_0
- 将局部变量表中的第 0 个元素 (变量 1) 压入操作数栈栈顶
-
iload_1
- 将局部变量表中的第 1 个元素 (变量 2) 压入操作数栈栈顶
-
iadd
- 将局部变量表中的第 0、1 个元素 (变量 1 和变量 2) 出栈,进行加法操作,并将结果 (变量 3) 再次压入操作数栈栈顶
-
istore_2
- 将局部变量表中的第 0 个元素 (变量 3) 出栈,将其存入局部变量表中,作为第 2 个元素,替代原先存入的常量 0
-
iload_2
- 将局部变量表中的第 2 个元素 (变量 3) 压入操作数栈栈顶
-
ireturn
- 返回操作数栈栈顶元素 (变量 3)
-
结果返回之后,方法执行完毕,所有栈帧会自动销毁
-
-
-
四、递归引发的 StackOverflowError 异常
-
如上所示,当一个线程执行一个方法时,就会创建一个对应的栈帧,并将建立的栈帧压入到虚拟机栈中,当方法执行完毕时,便会将栈帧出栈
-
因此可知线程当前执行的方法所对应的栈帧必定位于虚拟机栈的顶部,而递归函数会不断地调用自身,每一次方法调用都会涉及以下几点
-
每调用一次方法,就会生成一个栈帧
-
线程会保存当前方法的栈帧状态,并将它放到虚拟机栈中
-
当栈帧进行上下文切换时,会切换到最新的方法栈帧中
-
-
而我们每个虚拟机栈的深度是固定的,递归调用会导致栈深度的增加,每次递归都会往虚拟机栈中压入一个栈帧,如果该栈深度超出了虚拟机所允许的深度,就会报 StackOverflowError 异常
-
解决思路
-
限制递归的次数
-
可以使用循环的方法来替换递归
-
五、虚拟机栈过多引发的 OutOfMemoryError 异常
-
如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常
public void stackLeakByThread() { while(true) { new Thread() { @Override public void run() { while(true) { } } }.start(); } }
-
如上所示,理论上这段代码的运行结果应该是
-
但是由于在 Windows 平台的虚拟机中,Java 线程会映射到操作系统的内核线程上,执行上述代码,有较大风险会导致系统假死
-
六、归纳总结
-
Java 虚拟机栈
- 线程私有,生命周期与线程相同
-
栈帧
-
方法运行时的基础数据结构,与方法一一对应
-
用于存储局部变量表、操作数栈、动态链接、方法出口等信息
-
每一个方法从调用到执行完成的过程,对应着一个栈帧从虚拟机栈中入栈到出栈的过程
-
-
局部变量表
-
用于存放编译期可知的各种基本数据类型、对象引用以及 returnAdress 类型
-
基本数据类型
- char、byte、short、int、long、float、double、boolean
-
对象引用
-
可能是一个指向对象起始地址的引用指针
-
也可能是指向一个代表对象的句柄或其他与此对象相关的位置
-
-
returnAdress 类型
- 指向一条字节码指令的地址
-
-
局部变量表所需的内存空间在编译期间完成分配,在方法运行期间不会改变局部变量表的大小
-
-
虚拟机栈存在的两种异常
-
StackOverflowError
- 线程请求的栈深度大于虚拟机所允许的深度
-
OutOfMemoryError
- 虚拟机动态扩展时无法申请到足够的内存
-