5.1 概述
栈是运行时的单位,而堆是存储的单位。栈解决程序的运行问题,即程序怎么运行,如何处理数据。堆解决的是数据存储问题,即数据怎么放。
Java虚拟机栈(Java Virtual Machine Stack),Java栈。每个线程在创建时都会创建一个虚拟机栈,内部保存一个个的栈帧(Stack Frame),对应Java方法调用,生命周期和线程一致
主管Java程序的运行,保存方法的局部变量(8种基本类型、对象的引用地址)、部分结果,并参与方法的调用和返回
虚拟机中遇到的异常
Java虚拟机允许Java栈的大小是动态的或者是固定不变的
- 如果采用固定大小的Java虚拟机栈,那么每一个线程的Java虚拟机栈容量可以在线程创建时独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,那么会抛出一个StackOverflowError异常
- 如果Java虚拟机栈采用动态扩展,并且在尝试扩展的时候无法得到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,会抛出OOM异常
public class StackErrorTest {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count++);
main(args);
}
}
设置栈内存大小,参数-Xss
5.2 栈的存储单位
在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)
public class StackFrameTest {
public static void main(String[] args) {
method01();
}
private static int method01() {
System.out.println("方法1的开始");
int i = method02();
System.out.println("方法1的结束");
return i;
}
private static int method02() {
System.out.println("方法2的开始");
int i = method03();;
System.out.println("方法2的结束");
return i;
}
private static int method03() {
System.out.println("方法3的开始");
int i = 30;
System.out.println("方法3的结束");
return i;
}
}
输出结果
方法1的开始
方法2的开始
方法3的开始
方法3的结束
方法2的结束
方法1的结束
栈运行原理
- 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另一个线程的栈帧
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧。接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
- Java方法有两个返回函数的方式,一种是正常的函数返回,使用return指令;另一种是抛出异常,不管哪种方式,都会导致栈帧被弹出
5.3 局部变量表(Local Variables)本地变量表
定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型
由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中,在方法运行期间是不会改变局部变量表的大小
关于Slot的理解
局部变量表,最基本的存储单位Slot(变量槽)
在局部变量表里,32位以内的类型占用一个Slot,64位的l类型(long,double)占用两个Slot
JVM为局部变量表中的每一个Slot都分配一个访问索引,通过这个访问索引可成功访问到局部变量表中指定的局部变量值
当一个实例方法被调用时,他的方法参数和方法体内定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上
如果访问一个64位的局部变量时,只需要使用前一个索引即可
5.4 操作数栈(Operand Stack)表达式栈
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
每一个操作数栈都拥有一个明确的栈深度用于存储数值,所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_stack值
操作数栈只能通过标准的入栈和出栈来完成一次数据访问
5.5 代码追踪
public void testAddOperation() {
byte i = 15;
int j = 8;
int k = i + j;
}
javap -v .class
5.6 栈顶缓存(Top-of-Stack-Cashing)技术
由于操作数是储存在内存中的,因此频繁地执行内存读写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者提出了栈顶缓存(ToS)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低内存的读写次数,提升执行引擎的执行效率
5.7 动态链接(Dynamic Linking)指向运行时常量池的方法引用
每一个栈帧内部都包括一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就i是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如:invokedynamic指令
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
5.8 方法的调用:解析与分派
在JVM中,将符号饮用转换为调用方法的直接引用与方法的绑定机制相关
- 静态链接—>早期绑定
当一个字节码文件被装载到JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接 - 动态链接—>晚期绑定
如果被嗲用的方法在编译期无法确定下来,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,称为动态链接
非虚方法
如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这个方法称为非虚方法
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法
其他方法是虚方法
虚拟机中提供以下几条方法调用指令:
- 普通调用指令
1.invokestatic :调用静态方法,解析阶段确定唯一方法版本
2.invokespecial:调用《init》方法、私有及父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法- 动态调用指令
5.invokedynamic:动态解析初需要调用的方法,然后执行
方法重写的本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记住C
- 如果在类型C找那个找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接应用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常
- 否则,按照继承关系从下往上以此对C的各个父类进行第2步的搜索和验证过程
- 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常
5.9 方法返回地址(Return Address)方法正常退出或异常退出的定义
存放调用该方法的pc寄存器的值
一个方法的结束,有两种方式:
- 正常执行完成
- 出现未处理的异常,非正常退出
方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。异常退出时,返回地址是要通过异常表来确定,栈帧一般不会保存这部分信息
5.10 一些附加信息
例如,对程序调试提供支持的信息
5.11 相关面试题
- 举例栈溢出的情况(StackOverflowError)
通过-Xss设置栈的大小
- 调整栈大小,就能保证不出现溢出了吗?
不能
- 垃圾回收会涉及到虚拟机栈吗?
不会的
运行时数据区 | Error/GC |
程序计数器 | ×/× |
虚拟机栈 | √/× |
本地方法栈 | √/× |
方法区 | √/√ |
堆 | √/√ |
- 分配的栈内存越大越好吗?
不是的,一定时间内降低了OOM概率,但是会挤占其他的线程空间,整个空间是有限的
- 方法中定义的局部变量是否线程安全?
如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
public class StringBuilderTest {
// s1的声明方式是线程安全的
public static void method01() {
// 线程内部创建的,属于局部变量
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
// 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
// stringBuilder 是线程不安全的,操作的是共享数据
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
/**
* 同时并发的执行,会出现线程不安全的问题
*/
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
// StringBuilder是线程安全的,但是String也可能线程不安全的
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}