前言
文章奔着简洁易懂的形式去写,不会有很多花哨的废话,尽可能简明扼要的描述清楚想要表达的一些东西,如果你想深入了解JVM底层,不妨花几分钟仔细看看
昨天说过了 程序计数器 特点以及作用 今天接着说虚拟机栈
说起栈内存,可能很多人比较熟悉,什么栈内存溢出啊,递归啊,等。。。。。。
那今天就来带着大家一起详细的解读一下栈内存的相关知识
虚拟机栈
定义
Java Virtual Machine Stacks (Java 虚拟机栈)
每个线程运行时所需要的内存,称为虚拟机栈
每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法 什么又是活动栈帧呢?
活动栈帧就是代表这个线程正在执行的方法
我们都知道栈是先进后出的 线程运行需要的内存空间,那么每一个被调用的方法,又称之为栈帧 ,当方法调用完毕的时候,按照先进后出的顺序依次弹栈!
关系:一个栈由多个栈帧组成
接下来拿一段代码去进行探索:
/**
* 演示栈帧
*/
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;
}
}
debug模式,选择左侧Frames,代表这就是我们虚拟机栈的空间,可以看到根据我启动main方法,main栈帧被加载进栈内存,接着一步步运行,后调用的method1 和 method2 都加载进栈,并且先调用的main在最下面,后调用的在最上面,接着调用完毕对应的栈帧会出栈,变量参数等都会被释放掉
活动栈帧:就是在栈顶部,正在执行的这个方法,称之为活动栈帧
说完栈的基本概念,我们要来辨析一些问题!
看一个变量是否是线程安全的我总结:
到底是多个线程对这个变量共享还是这个变量对每个线程是私有的
提问:如果有多个线程同时调用这个方法,会不会存在X值的混乱呢?
/**
* 局部变量的线程安全问题
*/
public class Demo1_18 {
// 多个线程同时执行此方法
static void m1() {
int x = 0;
for (int i = 0; i < 5000; i++) {
x++;
}
System.out.println(x);
}
}
答案: 不会
为什么?
X是局部变量,相当于每个线程都有私有的局部遍历,所以各循环5000次互不干扰!
小结:这个案例中的X是线程私有的,所以并不会收到干扰
如果你将 x 值改为static 那么结果大不相同, 他是线程共享的数据,意味着线程1 和线程2 都要读取他
如果是这样: 那么此变量X 是多个线程共享的,就会存在线程安全问题
判断线程安全依据: 变量到底是多个线程对这个变量共享还是这个变量对每个线程是私有的
根据上述结论,可以探究一下以下代码是否线程安全
答案在注释中,自行理解
/**
* 局部变量的线程安全问题
*/
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();
}
//安全-->没有逃离方法的作用访问 无参 无返回
public static void m1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}
//不安全-->局部变量作为方法参数传入逃离方法的作用访问
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;
}
}
由此得出结论:
方法内的局部变量是否线程安全?
如果方法内局部变量没有逃离方法的作用访问,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
栈内存溢出
我这里总结了两点,也是开发经常遇到的情况
栈帧过多导致栈内存溢出 举例: 我的栈内存大小是固定的,可是我调用的栈帧(方法)多了 引起的栈内存溢出
最典型的就是不恰当的递归调用
一直调用没有出栈,直到某一次溢出 如下图
演示代码:
/**
* 演示栈内存溢出 java.lang.StackOverflowError
* -Xss256k
*/
public class Demo1_2 {
private static int count; //为了方便观看,计数
public static void main(String[] args) {
try {
method1();
} catch (Throwable e) {
e.printStackTrace();
System.out.println(count);
}
}
private static void method1() {
count++;
method1();
}
}
也可以加虚拟机参数,指定栈内存稍微小一点
栈帧过大导致栈内存溢出
这个不太好演示,因为方法内部的局部变量占用内存都是比较小的,一个int是4个字节,栈的默认大小在1兆左右。
线程运行诊断
定位:
用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
Linux环境演示:
top 定位到进程ID
然后再用ps查看线程对cpu的占用情况
展示的是当前Linux系统所有的线程-->寻找你想要的
如果线程很多就用 | grep 筛查
jstack 进程id
先将32665(当前十进制) 线程编号 转换为16进制 7F99
根据7F99去找
可以看到他的运行状态一直为runnable 所以占用了cpu很多资源,并且显示Java源码第八行有错
真是这里的while(true) 导致CPU占用过高
这样我们就分别演示了 栈内存溢出 以及 线程运行诊断
总结:
栈内存溢出
- 栈帧过多导致栈内存溢出
- 栈帧过大导致栈内存溢出
诊断定位:
用top定位哪个进程对cpu的占用过高
ps H -eo pid,tid,%cpu | grep 进程id (用ps命令进一步定位是哪个线程引起的cpu占用过高)
jstack 进程id 可以根据线程id 找到有问题的线程,进一步定位到问题代码的源码行号
虚拟机栈讲解到此,之后接着说 本地方法栈
希望同仁志士,前来参考以及指点!共同进步,发扬文化精神!转载请标明出处!
感觉不错的点个赞关注一下吧!