与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

栈绝对算的上是 JVM 运行时数据区域的一个核心,除了一些 Native 方法调用是通过本地方法栈实现的(后面会提到),其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
image.png

局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。

动态链接 主要服务一个方法需要调用其他方法的场景。在 Java 源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用(Symbilic Reference)保存在 Class 文件的常量池里。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用。

栈空间虽然不是无限的,但一般正常调用的情况下是不会出现问题的。不过,如果函数调用陷入无限循环的话,就会导致栈中被压入太多栈帧而占用太多空间,导致栈空间过深。那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
简单总结一下程序运行中栈可能会出现两种错误:

  • StackOverFlowError: 若栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
  • OutOfMemoryError: 如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

image.png

Q1:垃圾回收是否涉及到栈内存?
不涉及。栈内存是方法调用是分配的,在方法结束调用后,就将栈帧弹出栈了,释放了内存。

Q2:栈内存分配越大越好吗?
并不是。系统的物理内存是一定的,栈空间越大,会导致线程数越少。栈空间越大,也并不会让程序更快,只是有更大的栈空间,能让你做更多次的递归调用。

Q3:方法内的局部变量是否线程安全?

  1. 判断是否安全,即看这些变量对于多个线程是共享的还是私有的。
  2. 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
  3. 如果是局部变量引用了对象,并逃离方法的作用方法,需要考虑线程安全 ```java 对于局部变量int x 来说,它是方法内的局部变量,在多线程下,每个线程会为其运行的方法分配栈和栈帧,栈是线程私有的,方法中的局部变量不会受其他线程影响。

static void m1() { int x = 0; for (int i = 0; i < 5000; i++) { x++; } System.out.println(x); }

  1. ```java
  2. 每个线程读取static变量到线程的工作内存中,然后再进行计算,最后将修改以后的变量的值同步到static变量中,因此需要考虑线程安全问题。
  3. static int x = 0;
  4. static void m1() {
  5. for (int i = 0; i < 5000; i++) {
  6. x++;
  7. }
  8. System.out.println(x);
  9. }
  1. // 方法线程安全:局部变量在方法内,线程私有。
  2. public static void m1() {
  3. StringBuilder sb = new StringBuilder();
  4. sb.append(1);
  5. sb.append(2);
  6. sb.append(3);
  7. System.out.println(sb.toString());
  8. }
  9. // 方法线程不安全:变量是通过参数拿到的,那么与此同时其他线程也可能拿到这个参数然后对其进行修改。
  10. public static void m2(StringBuilder sb) {
  11. sb.append(1);
  12. sb.append(2);
  13. sb.append(3);
  14. System.out.println(sb.toString());
  15. }
  16. // 方法线程不安全:局部变量虽然声明在方法内,但是在最后确返回出去了。返回出去后可能被其他线程引用进行修改。
  17. public static StringBuilder m3() {
  18. StringBuilder sb = new StringBuilder();
  19. sb.append(1);
  20. sb.append(2);
  21. sb.append(3);
  22. return sb;
  23. }

栈内存溢出

栈空间调整参数

-Xss空间大小
-Xss8M

1. 栈帧过多导致栈内存溢出

image.png
当程序递归调用次数太多时,会超出栈的空间,导致栈内存溢出。
image.png

2. 栈帧过大导致栈内存溢出

image.png
方法携带的参数等占用内存太多,导致栈帧过大,使栈内存溢出。

  1. **
  2. * json 数据转换, 对象嵌套导致栈帧过大
  3. */
  4. public class Demo1_19 {
  5. public static void main(String[] args) throws JsonProcessingException {
  6. Dept d = new Dept();
  7. d.setName("Market");
  8. Emp e1 = new Emp();
  9. e1.setName("zhang");
  10. e1.setDept(d);
  11. Emp e2 = new Emp();
  12. e2.setName("li");
  13. e2.setDept(d);
  14. d.setEmps(Arrays.asList(e1, e2));
  15. // { name: 'Market', emps: [{ name:'zhang', dept:{ name:'', emps: [ {}]} },] }
  16. ObjectMapper mapper = new ObjectMapper();
  17. System.out.println(mapper.writeValueAsString(d));
  18. }
  19. }
  20. class Emp {
  21. private String name;
  22. @JsonIgnore
  23. private Dept dept;
  24. public String getName() {
  25. return name;
  26. }
  27. public void setName(String name) {
  28. this.name = name;
  29. }
  30. public Dept getDept() {
  31. return dept;
  32. }
  33. public void setDept(Dept dept) {
  34. this.dept = dept;
  35. }
  36. }
  37. class Dept {
  38. private String name;
  39. private List<Emp> emps;
  40. public String getName() {
  41. return name;
  42. }
  43. public void setName(String name) {
  44. this.name = name;
  45. }
  46. public List<Emp> getEmps() {
  47. return emps;
  48. }
  49. public void setEmps(List<Emp> emps) {
  50. this.emps = emps;
  51. }
  52. }

线程运行诊断

案例一:CPU占用过高

  1. # 使用top命令查看当前cup运行情况
  2. top
  3. # 用PS命令进一步定位是哪个线程引起的CPU占用过高
  4. ps H -eo pid,tid,%cpu|grep 进程ID
  5. # 该指令是JVM指令
  6. jstack 进程ID

top
image.png

ps H -eo pid,tid,%cpu | grep 32655 image.png

jstack 32655 image.png

案例二:程序运行很长时间没有结果

jstack 32275 image.png