1 栈
栈是一种“操作受限”的线性表,只涉及在一端插入和删除数据,并且满足后进先出、先进后出的特性。
2 Java虚拟机栈
2.1 定义
Java 虚拟机栈描述的是 Java 方法执行的内存模型,用于存储栈帧。线程启动时会创建虚拟机栈,每个方法在执行时会在虚拟机栈中创建一个栈帧,用于存储局部变量表、操作数栈、动态连接、方法返回地址、附加信息等信息。每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中的入栈(压栈)到出栈(弹栈)的过程。
- 每个线程运行需要的内存空间,称为虚拟机栈;
- 每个栈由多个栈帧组成,对应着每次方法调用所占用的内存;
- 栈帧:每个方法运行时需要的内存,一个栈帧表示一个方法的调用;
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法;
2.2 实例
说明:设置一个简单的Demo,主函数调用method1,method1调用method2,我们观察每个方法入栈的顺序及其它们之间的先后顺序。
public class Demo1_1 {
public static void main(String[] args) throws InterruptedException {
method1();
}
private static void method1() {
method2(1, 2);
}
private static int method2(int a, int b) {
int c = a + b;
return c;
}
}
1.程序进入主函数,因此需要先把主函数放入栈顶。
2.主函数调用method1,因此将method1压入栈顶。
3.method1调用method2,将method2压入栈顶
4.执行完成后,依次从栈顶将栈帧取出,并释放。
3 栈内存溢出
- 栈帧过多导致溢出(递归调用未设置结束条件,导致栈帧不断放入栈,无法释放)
- 栈帧过大导致溢出
4 问题
1. 垃圾回收是否涉及栈内存?
不需要,每个方法在出栈后自动释放;垃圾回收针对堆内存中的无用对象进行回收。
2. 栈内存分配越大越好吗?
不是,栈内存过大,会使线程数变少,会影响运行效率,所以正常情况下采用默认值即可。
不同环境中的默认值:
Linux:1024
macos:1024
Oracle Solaris: 1024
Windows:依赖于虚拟内存的默认值
3. 方法内的局部变量是否线程安全?
- 如果方法局部变量没有逃离方法的作用范围,它是线程安全的;
- 如果局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全;
实例1:
/**
* 局部变量的线程安全问题
*/
public class Demo1_18 {
// 多个线程同时执行此方法,是否会出现X值紊乱的情况
static void m1() {
int x = 0;
for (int i = 0; i < 5000; i++) {
x++;
}
System.out.println(x);
}
}
答:不会出现紊乱情况
原因:每个线程都有一个单独的虚拟机栈,而且x属于局部变量,隶属于Demo1_18这个方法,运行时以栈帧形式放入栈中,所以在时间运行过程中是在不同栈空间的栈帧里运行的,所以不会影响。
实例2
/**
* 局部变量的线程安全问题
*/
public class Demo1_17 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(()->{
m2(sb);
}).start();
}
//m1是线程安全的
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//m2不是线程安全的,因为参数对象可能被其他线程引用,造成结果紊乱
public static void m2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
public static StringBuilder m3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}