文章目录


1.定义

Java Virtual Machine Stacks (Java 虚拟机栈)
JVM专题(三)-虚拟机栈_开发语言

  • 每个线程运行需要的内存空间,称为虚拟机栈
  • 每个栈由多个栈帧组成,对应着每次调用方法时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的方法

2.演示

public class Main {
public static void main(String[] args) {
method1();
}

private static void method1() {
method2(1, 2);
}

private static int method2(int a, int b) {
int c = a + b;
return c;
}
}

流程分析:
JVM专题(三)-虚拟机栈_开发语言_02

我们来打断点来Debug一下看一下方法执行的流程:
JVM专题(三)-虚拟机栈_开发语言_03
在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点

3.问题辨析

3.1.垃圾回收是否涉及栈内存?

不需要。因为虚拟机栈中是由一个个栈帧组成的,在方法执行完毕后,对应的栈帧就会被弹出栈。所以无需通过垃圾回收机制去回收内存。

3.2.栈内存的分配越大越好吗?

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。
JVM专题(三)-虚拟机栈_栈内存_04

举例:如果物理内存是500M(假设),如果一个线程所能分配的栈内存为2M的话,那么可以有250个线程。而如果一个线程分配栈内存占5M的话,那么最多只能有100个线程同时执行!所以栈内存划分大了只会导致可运行的线程数目变少。

3.3.方法内的局部变量是否是线程安全的?

JVM专题(三)-虚拟机栈_线程安全_05
情况1:
JVM专题(三)-虚拟机栈_线程安全_06
情况2:如果你把int x变为static就会出现线程安全问题:
JVM专题(三)-虚拟机栈_线程安全_07

从图中得出:局部变量如果是静态的可以被多个线程共享,那么就存在线程安全问题。如果是非静态的只存在于某个方法作用范围内,被线程私有,那么就是线程安全的!

再来看一个案例:

/**
* 局部变量的线程安全问题
*/
public class Demo02 {
public static void main(String[] args) {// main 函数主线程
StringBuilder sb = new StringBuilder();
sb.append(4);
sb.append(5);
sb.append(6);
new Thread(() -> {// Thread新创建的线程
m2(sb);
}).start();
}

public static void m1() {
// sb 作为方法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 作为方法m2()外部的传递来的参数,sb 不在方法m2()的作用范围内
// 不是线程私有的 ---> 非线程安全
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb.toString());
}

public static StringBuilder m3() {
// sb 作为方法m3()内部的局部变量,是线程私有的
StringBuilder sb = new StringBuilder();// sb 为引用类型的变量
sb.append(1);
sb.append(2);
sb.append(3);
return sb;// 然而方法m3()将sb返回,sb逃离了方法m3()的作用范围,且sb是引用类型的变量
// 其他线程也可以拿到该变量的 ---> 非线程安全

// 如果sb是非引用类型,即基本类型(int/char/float...)变量的话,逃离m3()作用范围后,则不会存在线程安全
}
}

JVM专题(三)-虚拟机栈_开发语言_08
小结:

  • 如果方法内局部变量没有逃离方法的作用范围,则是线程安全的
  • 如果局部变量引用了对象(像上面的​​StringBulider​​就是引用了一个对象),并逃离了方法的作用范围,则需要考虑线程安全问题
  • 如果局部变量只是基本类型变量(没有引用对象),并逃离了方法的作用范围,则不存在线程安全问题

3.4.栈内存溢出

​Java.lang.stackOverflowError​​ 栈内存溢出

  • 虚拟机栈中,栈帧过多(方法无限递归)导致栈内存溢出,这种情况比较常见!
  • 每个栈帧所占用内存过大(某个/某几个栈帧内存直接超过虚拟机栈最大内存),这种情况比较少见!

如图所示,就是栈中栈帧过多的情况:
JVM专题(三)-虚拟机栈_后端_09
演示

/**
* 演示栈内存溢出 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();
}
}

执行次数:
JVM专题(三)-虚拟机栈_后端_10
JVM专题(三)-虚拟机栈_开发语言_11
当我们将虚拟机栈内存缩小到指定的256k的时候再运行Demo1_2后,会得到其栈内最大栈帧数为:2928远小于原来的23040!

栈溢出案例:

package com.stack;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Arrays;
import java.util.List;

/**
* json 数据转换
*/
public class Demo1_19 {

public static void main(String[] args) throws JsonProcessingException {
Dept d = new Dept();
d.setName("Market");

Emp e1 = new Emp();
e1.setName("zhang");
e1.setDept(d);

Emp e2 = new Emp();
e2.setName("li");
e2.setDept(d);

d.setEmps(Arrays.asList(e1, e2));

// { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(d));
}
}

class Emp {

private String name;
private Dept dept;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}

public Dept getDept() {
return dept;
}

public void setDept(Dept dept) {
this.dept = dept;
}
}
class Dept {
private String name;
private List<Emp> emps;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Emp> getEmps() {
return emps;
}

public void setEmps(List<Emp> emps) {
this.emps = emps;
}
}

@JsonIgnore注解可以解除JSON转换循环问题

3.5.线程诊断_CPU占用过高

Linux环境下运行某些程序的时候,可能导致CPU的占用过高,这时需要定位占用CPU过高的线程

package com.stack;

/**
* 演示 cpu 占用过高
*/
public class Demo1_16 {

public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("1...");
while (true) {

}
}, "thread1").start();


new Thread(null, () -> {
System.out.println("2...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();

new Thread(null, () -> {
System.out.println("3...");
try {
Thread.sleep(1000000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}

JVM专题(三)-虚拟机栈_开发语言_12

  • top命令,查看是哪个进程占用CPU过高
    JVM专题(三)-虚拟机栈_后端_13
  • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步查看是哪个线程占用CPU过高
    JVM专题(三)-虚拟机栈_java_14
  • jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换
    JVM专题(三)-虚拟机栈_线程安全_15

3.6.线程诊断_迟迟得不到结果

package com.stack;

/**
* 演示线程死锁
*/
class A{};
class B{};

public class Demo1_3 {

static A a = new A();
static B b = new B();

public static void main(String[] args) throws InterruptedException {
new Thread(()->{
synchronized (a) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
Thread.sleep(1000);
new Thread(()->{
synchronized (b) {
synchronized (a) {
System.out.println("我获得了 a 和 b");
}
}
}).start();
}

}

JVM专题(三)-虚拟机栈_开发语言_16
JVM专题(三)-虚拟机栈_java_17
JVM专题(三)-虚拟机栈_后端_18