虚拟机栈详解
- 虚拟机栈
虚拟机栈
上一篇文章,我们简单介绍了一下虚拟机栈,虚拟机栈涉及的内容很多,所以我们另外再专门开一篇文章来讲述一下虚拟机栈。我们在写代码的过程中,经常会遇见Java程序异常的时候,通常这个时候我们的程序会打印出一系列堆栈信息。而我们通过这些堆栈信息就可以很清楚的知道我们的方法的调用链路,进而排查问题。堆栈是由栈帧组成,而栈帧里面又包含局部变量表 操作数栈 动态链接 方法返回地址等信息组成。下面我们将对这些内容进行详细介绍,希望通过我的介绍能让大家对虚拟机栈有一个比较深入的理解。
概述
Java语言由于跨平台性,所以指令集是以栈为基础设计的。每个线程创建的时候都会创建一个虚拟机栈,内部每一个栈都是由一系列栈帧组成,每一个栈帧又对应了一个方法调用。虚拟机栈跟数据结构中的栈有类似的特点,都是先进后出,只支持入栈与出栈两种操作。另外栈是线程私有的。
案例
java
public class StackTest {
public void test1() {
int a = 0;
int b = 0;
test2();
}
public void test2() {
int a = 0;
int b = 0;
test3();
}
public void test3() {
int a = 0;
int b = 0;
}
public static void main(String[] args) {
StackTest stackTest = new StackTest();
stackTest.test1();
}
}

虚拟机栈保存方法的局部变量[1](#1)和方法调用返回结果。虚拟机栈不存在垃圾回收问题,但是存在内存溢出问题,栈先进后出,每个新的放啊执行的时候都要伴随着压栈操作;方法执行结束后,伴随着出栈操作。
常见的跟虚拟栈异常相关的异常
虚拟机栈内存溢出问题常见的异常有两种:
- 如果是固定大小的虚拟机栈,虚拟机栈的大小固定,因为每一个新方法的调用都伴随着压栈操作,当新方法足够多的时候就可能把固定大小虚拟机栈内存用完,这个时候就会爆出StackOverflowError异常
- 如果虚拟机栈大小可以动态变化,但是尝试获取新的内存空间的时候,没有内存资源的时候,JVM就会抛出OutOfMemoryError异常
StackOverflowError异常
代码案例:
java
public class StackOverflowError {
public static int counter = 0;
public void setCounter() {
counter++;
setCounter();
}
public static void main(String[] args) {
StackOverflowError stackOverflowError = new StackOverflowError();
try {
stackOverflowError.setCounter();
} catch (Throwable e) {
System.out.println("调用深度:" + counter);
e.printStackTrace();
}
}
}
我们可以通过设置-Xss参数来设置线程的最大虚拟机栈的大小,栈的大小会直接影响调用栈的深度。

-
Xss设置为256k的时候
-
Xss设置为512K的时候
可以明显看到,我们的虚拟机栈的大小扩大之后,调用栈的深度也随之扩大。
OutOfMemoryError异常
代码案例:
java
public class OutOfMemoryError {
public static void main(String[] args) {
int i = 0;
while (true) {
Thread thread = new MyThread();
thread.start();
}
}
}
class MyThread extends Thread {
CountDownLatch startSignal = new CountDownLatch(1);
public void run() {
while (true) {
try {
startSignal.await();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
栈的基本存储单位
每个线程都有自己的栈,栈中的数据都是以栈帧的形式存在。这个线程上执行的每个方法都各自对应一个栈帧[2](#2)。在一个运行状态的线程中,一个时间点上只会有一个活动的栈帧即栈顶栈帧在活动。之前说过的,每一个栈帧都对应的一个方法,如果当前方法执行新的方法,那又会有新的栈帧被创建出来,放在栈顶成为新的栈帧。
局部变量表
局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法体内部的局部变量,数据类型包括基本数据类型、引用数据类型以及returnAddress类型。基本数据类型直接存储的是其值;引用数据类型存储的是对对象的引用。
局部变量是属于栈帧,而栈帧又属于虚拟机栈,因而局部变量是线程私有的,不存在并发的数据安全问题。
局部变量表所需要的容量大小是编译期就确定的,保存在方法的code属性的maximum local variables数据项中。局部变量是按照运行期间所需要的最大大小确定的,运行期间不会改变它的大小。
IDEA Jclasslib Bytecode Viewer插件
通过IntelliJ IDEA安装Jclasslib Bytecode Viewer插件可以查看局部变量表。安装好插件以后,单击"View"选项,选择"Show Bytecode With Jclasslib"选项

slot
局部变量的基本存储单位是slot(变量槽)。在局部变量表里面,32位以内的基本类型占用一个slot,64位的类型占用两个slot。byte、short、char在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。long和double则占据两个slot。JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
当一个实例方法被调用的时候,它的方法参数与方法体内部的定义的局部变量将会按照顺序被复制到局部变量中的每一个slot上去。如果方法是实例方法,slot 0 处放的是this指针。
另外局部变量中的slot可以重用,局部变量过了作用域之后,后面的新的局部变量可以占用它的位置。
操作数栈
栈帧除了包含局部变量之后,还包含一个操作数栈。它主要用于保存计算过程的中间结果,同时作为计算过程中变量的临时存储空间。因为它也是栈结构,所以只又进栈与出栈两种操作。每一个操作数栈的深度在编译器就确定了,保存在Maximum stack size数据中。我们之前说JVM是基于栈的,这个栈指的就是操作数栈。