1、定义:
- 每个
线程运行需要的内存空间
,称为虚拟机栈;- 每个栈由多个
栈帧组成
,对应着每次方法调用
时所占用的内存空间;- 每个线程只能有一个活动栈帧,对应当前正在执行的那个方法,通常指栈顶的栈帧;
代码演示
package com.jvm.stack;
/**
* @author JohnGea
* @date 2022-06-03 2:05 下午
*/
@SuppressWarnings("all")
public class JvmStack {
public static void main(String[] args) {
method1();
}
public static void method1(){
method2(1,2);
}
public static void method2(int a,int b){
a = a + b;
System.out.println(a);
}
}
我们调试这段代码,在IDEA的左下角可以看到,该程序运行时所对应的虚拟机栈的情况:
2、常见问题
1.垃圾回收是否涉及到栈内存?
- 不需要,因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。
2.栈内存分配越大越好吗?
- 不一定,由于我们计算机的内存大小是固定的,如果对单个栈的内存设置的大小越大,就会导致系统能执行的线程数越少。(就好比计算一个四边形的面积,如果对四边形的长设置的越大,就会导致宽越小,而面积是恒定不变的)
3.方法内的局部变量是否线程安全?
- 如果方法内部局部变量没有逃离方法的作用访问,它是线程安全的。
- 如果是局部变量引用了对象,并逃离方法的范围,需要考虑线程安全问题。
我们对问题3进行举例:
public class main1 {
public static void main(String[] args) {
}
//下面各个方法会不会造成线程安全问题?
//不会
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;
//该对象由于被方法return了,其他线程可能会拿到这个对象进行操作
}
}
3、栈内存溢出
由于栈内存是有限的,而栈内存中存放的又是各个方法的调用,因此,当一些方法调用到一定深度时,会导致栈帧过多,栈内存已满,从而出现Java.lang.stackOverflowError
问题,如下图所示。
同样,如果栈帧过大,也可能导致栈内存溢出,不过该情况比较少见,如下图所示。
通过代码模拟栈内存溢出
下面我们通过方法的死递归调用来模拟栈内存溢出问题。
package com.jvm.stack;
/**
* @author JohnGea
* @date 2022-06-02 11:09 下午
* 演示栈内存溢出问题 java.lang.StackOverFlowError
* -Xss384k(设置虚拟机参数,将栈内存大小修改为384k,默认是1024k)
*/
public class JvmStackFrame {
private static int count;//计算栈内的栈帧数量
public static void main(String[] args){
try {
method2();
}
catch (Throwable e){
e.printStackTrace();
System.out.println(count);
}
}
public static void method2(){
count++;
//方法递归调用,容易导致单个栈内栈帧过多,栈帧溢出
//方法递归调用,若栈帧过大,也会导致栈帧溢出
method2();
}
}
我们设置虚拟机参数-Xss384k
(设置虚拟机参数,将栈内存大小修改为384k,默认是1024k),运行代码,可以看到出现了java.lang.StackOverflowError
异常,最终栈内存放了4027
个栈帧后再没有新的空间可以存放了。
4、虚拟机栈出现OOM问题
不同于
java.lang.StackOverflowError
异常,虚拟机栈出现OOM是指在JVM中,有大量的线程正在运行,且不断有新的线程创建,导致JVM没有更多的空间去创建新的栈
(StackOverflowError是栈没有空间创建新的栈帧,而栈内存的OOM问题是没有新的空间创建栈)。
导致该问题的原因
- 大量线程处于运行状态,且不断有新的线程被创建;
- 单个栈内存的空间设置的太大,导致可创建的线程数有限;
(总之,虚拟机栈出现OOM就是由于无法运行更多的线程而导致的)
通过代码模拟虚拟机栈OOM的问题
package com.jvm.stack;
/**
* @author JohnGea
* @date 2022-06-03 12:17 上午
* 演示虚拟机栈导致OOM问题,虚拟机中栈过多(线程过多),导致没有空间再创建新的线程
* -Xss100M(设置虚拟机参数,将栈内存大小修改为100M,默认是1024k)
*/
public class JvmStackOOM {
public static void main(String[] args) {
//死循环创建线程,导致虚拟机中栈的数量过多,
//且线程又未释放空间,导致新的线程没有空间进行创建
int count = 0; //记录创建的线程数
try {
while (true){
new Thread(()->{
while (true){
}
}).start();
count++;
}
}catch (Throwable e){
e.printStackTrace();
System.out.println(count);
}
}
}
我们设置虚拟机参数-Xss100M
,运行代码,可以看到出现了java.lang.OutOfMemoryError
异常,最终程序创建了4074个线程后再没有新的空间来创建线程了。
5、线程运行诊断
案例一:CPU占用过高
Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程。
首先,我们运行下面的代码:
package com.jvm.stack;
/**
* @author JohnGea
* @date 2022-06-02 11:47 下午
* 检查某个进程占用CPU过多
*
* 1、(定位进程)通过 top 命令来查看系统那个进程占用CPU过高,从而得到进程的pid
* 2、(定位线程)通过 ps H -eo pid,tid,%cpu | grep 进程id 命令来查看该进程下哪个线程导致cpu占用过高
* 3、(定位代码)通过 jstack 进程id 命令来查看进程中的哪个线程调用的代码导致占用率过高
*/
public class JvmStackThreadCPU {
public static void main(String[] args) {
new Thread(()->{
System.out.println(1);
while (true){
}
},"thread01").start();
new Thread(()->{
System.out.println(2);
},"thread02").start();
new Thread(()->{
System.out.println(3);
},"thread02").start();
}
}
这时,我们打开终端,使用top
命令查看系统中进程的状态
1、(定位进程)通过 top 命令来查看系统那个进程占用CPU过高,从而得到进程的pid,可以看到这里有一个进程id为28567的进程占满了CPU;
2、(定位线程)通过 ps H -eo pid,tid,%cpu | grep 进程id 命令来查看该进程下哪个线程导致cpu占用过高;
3、(定位代码)通过 jstack 进程id 命令来查看进程中的导致CPU占用过高的线程id,并查看导致CPU占用过高的代码行号;
在控制台输入jstack 28567
,可以看到如下图所示,我们定位到自己创建的线程thread01
通过上图,我们看到thread01处于runnable状态,导致CPU过高的代码出现在17行。最后,我们回到代码中,查看通过jstack定位到的代码行——17行。
可以看到,是由于死循环,导致了thread1的CPU占用率很高。
案例二:线程运行迟迟等不到运行结果(发生死锁)
首先,我们运行如下代码:
package com.jvm.stack;
/**
* @author JohnGea
* @date 2022-06-03 12:05 上午
* 线程运行迟迟等不到运行结果(发生死锁)
* 1、(定位进程)top
* 2、(定位代码)jstack 进程id
*/
class A{}
class B{}
public class JvmStackThreadDeadLock {
static A a = new A();
static B b = new B();
public static void main(String[] args) {
new Thread(()->{
synchronized (a){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println("thread1拿到了a和b");
}
}
},"thread1").start();
new Thread(()->{
synchronized (b){
synchronized (a){
System.out.println("thread2拿到了a和b");
}
}
},"thread2").start();
}
}
同样,我们打开终端,使用top
命令查看系统中进程的状态。
定位到运行的进程后,我们依然可以通过jstack 进程id
的方式查看该进程的运行状态:
可以看到,jstack帮我们定位到了一个死锁问题,以及死锁产生的原因和对应的代码行号。我们回到代码中查看。
可以看到,两个线程都拿到了彼此需要的对象,但是又没有线程释放锁,结果导致进程一直处于运行状态而没有结果。