JVM 运行时内存空间详解——虚拟机栈
Posted 格子衫111
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM 运行时内存空间详解——虚拟机栈相关的知识,希望对你有一定的参考价值。
通过上一篇文章,我们大体了解了JVM的整体架构,其分为:元数据(JDK7是方法区)、堆、虚拟机栈、本地方法栈、程序计数器几个部分。
本篇文章,咱们对虚拟机器栈进行剖析,一探究竟。
1.什么是虚拟机栈
Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,即生命周期和线程相同。Java虚拟机栈和线程同时创建,用于存储栈帧。每个方法在执行时都会创建一个栈帧(Stack Frame),用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。每一个方法从调用直到执行完成的过程就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
概念不好理解?那么通过代码方式来看下,
java代码:
public class StackDemo {
public static void main(String[] args) {
StackDemo sd = new StackDemo();
sd.A();
}
public void A(){
int a = 10;
System.out.println(" method A start");
System.out.println(a);
B();
System.out.println("method A end");
}
public void B(){
int b = 20;
System.out.println(" method B start");
C();
System.out.println("method B end");
}
private void C() {
int c = 30;
System.out.println(" method C start");
System.out.println("method C end");
}
}
在如上示例代码中,main方法中调用A方法,A方法调用B方法,B方法调用C方法。
main() -> A() -> B() -> C()
通过Debug可以看到,方法被调用时,会进行一个压栈(进栈)的操作。方法被执行完毕时,会进行弹栈(出栈)。
遵循先进后出的规则:
压栈过程:main() -> A() -> B() -> C()
弹栈过程:C() -> B() -> A() -> main()
上图中,可以看到,方法从压栈到弹栈的过程,都会有一个对应的栈帧。那么,栈帧到底是什么呢?
2.什么是栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
3.设置虚拟机栈的大小
-Xss 为jvm启动的每个线程分配的内存大小,默认JDK1.4中是256K,JDK1.5+中是1M。
- Linux/x64 (64-bit): 1024 KB
- macOS (64-bit): 1024 KB
- Oracle Solaris/x64 (64-bit): 1024 KB
- Windows: The default value depends on virtual memory
-Xss1m —— 以m为单位
-Xss1024k—— 以k为单位
-Xss1048576—— 以字节为单位
下面是一个导致栈溢出的程序示例:
public class StackTest {
static long count = 0 ;
public static void main(String[] args) {
count++;
System.out.println(count); //默认栈内存,运行到8681
main(args);
}
}
可以看到,这是一个无限递归的方法,main方法会无限的进行压栈,最终会导致栈内存溢出。
如果设置的虚拟机栈内存大一点,则产生溢出的时间长一些,count的值会大一点。如果设置得比较小,则很快会溢出。
Windows的默认栈内存下,在IDEA中运行,当count 累加到8681的时候,就出现了栈内存溢出异常(StackOverflowError )。
然后,我们可以在run的位置,右键设置栈内存的大小。
修改成256k(肯定比默认的小很多)
再次运行
会溢出的次数变成了2047,比默认的情况小。
tips:
我们知道如何在IDEA中设置栈内存大小。但是,我们一般不需要设置栈内存,使用原来的默认值即可。因为在这种无限递归的情况下,方法会无限进栈,无论栈内存设置得多大,最终都会导致内存溢出,只是时间早一点晚一点而已。
4.局部变量表
局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数 和方法内定义的局部变量 。包括8种基本数据类型、对象引用(reference类型)和returnAddress类型(指向一条字节码指令的地址)。
其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余的数据类型只占用1个。
下面,通过代码来了解下其在字节码文件是如何存储的。
Java代码:
public class PCRegister {
public static void main(String[] args) {
int x = 1;
int y = 2;
System.out.println(x+y);
}
}
查看反编译的字节码文件,主要看LineNumberTable(行号编号表)和LocalVariableTable(局部变量表):
解析:
LineNumberTable中,
StartPC :代表编译后的字节码指令地址;
LineNum:对应Java源代码中对应的行号。
LocalVariableTable中,
StartPC :代表编译后的字节码指令地址;
Length :代表有效长度范围;
Index :代表对应的索引值;
Name : 代表变量名称。
5.操作数栈
操作数栈(Operand Stack)也称作操作栈,是一个后入先出栈(LIFO)。随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。
通过以下代码演示操作栈执行
Java代码:
public class StackDemo2 {
public static void main(String[] args) {
int i = 1;
int j = 2;
int z = i + j;
}
}
对反编译的字节码指令进行分析:
指令6,将两个数进行相加。指令7,将相加的结果存储起来,也就是将操作数栈中的计算结果3 加载到 局部变量表。
总结:
操作数栈,会把加载好的值放到局部变量表中。或者从局部变量表中加载一些值到操作数栈中做一下运算。
操作数栈是一个动态存储操作值的空间。
6.动态链接
Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程中的动态链接(Dynamic Linking)。
程序示例:
public class DynamicLink {
public static void main(String[] args) {
Math.random();
}
}
反编译字节码文件的指令内容:
点击符号引用,即可跳转到常量池中。
动态链接的作用:将符号引用转换成直接引用。
7.方法返回地址
方法返回地址存放调用该方法的PC寄存器的值。一个方法的结束,有两种方式:正常地执行完成,出现未处理的异常非正常的退出。
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。
方法正常退出时,调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
无论方法是否正常完成,都需要返回到方法被调用的位置,程序才能继续进行。
如上图所示,方法2 调用 方法1,所以方法2先入栈,方法1后入栈。方法1被执行正常执行完毕后,返回PC寄存器下次要执行的指令值,也就是方法2对应的PC寄存器的值。
所以,可以说,被调用的方法 保存的是调用方的PC寄存器的值。
通过异常退出的情况,则可以查看异常表,异常数据记录位置如下图所示:
以上是关于JVM 运行时内存空间详解——虚拟机栈的主要内容,如果未能解决你的问题,请参考以下文章