2. Java虚拟机栈

image.png

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链表、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。 经常有人把Java内存区域笼统的划分为堆内存(Heap)和栈内存(Stack),这种划分方式直接继承自传统的C、C++程序的内部布局结构,在Java语言里就显得有点粗糙了,实际的内存区域划分比这个更复杂。不过这种划分方式的流行也间接说明了程序员最关注的、与对象内存分配关系最密切的区域是“堆”和“栈”两块。其中,“堆”在稍后笔者会专门讲述,而“栈”通常就是指这里将的虚拟机栈,或者更多情况下只是虚拟机栈中的局部变量表部分。

2.1. 定义

  • Java Virtual Machine Stack (Java虚拟机栈);
  • 每个线程运行时所需要的内存,称为虚拟机栈;
  • 每个栈有多个栈帧(Farme)组成,对应这每次方法调用时所占用的内存;
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法。

    2.1.1. 举例说明

    ```java /**

    • 演示栈帧
    • @author : gnehcgnaw
    • @since : 2020/4/9 18:05 */ public class Demo1 { public static void main(String[] args) {

      1. int m = method1(1);
      2. System.out.println(m);

      }

      private static int method1(int i) {

      1. int y = i +1;
      2. int z = method2(y);
      3. return z ;

      }

      private static int method2(int y) {

      1. int m = y+1;
      2. return m;

      }

}

<a name="wN1wM"></a>
## ![2020-04-09_18-10-37 (1).gif](https://cdn.nlark.com/yuque/0/2020/gif/285619/1586427179942-0e1f4a00-63b2-464d-a1d5-3ba2bdbf0d4a.gif#align=left&display=inline&height=943&margin=%5Bobject%20Object%5D&name=2020-04-09_18-10-37%20%281%29.gif&originHeight=943&originWidth=1680&size=3508576&status=done&style=none&width=1680)
上图idea中的Frames是栈,main、method1、method2是栈帧,程序的运行是一个压栈、出栈的过程,variables是局部变量表。
<a name="pknJj"></a>
## 2.2. 问题辨析
<a name="m6fTT"></a>
### 2.2.1. 垃圾回收是否涉及栈内存?

- 不需要,因为我们的栈内存,无非就是一次次的方法调用所产生的栈帧内存,而栈帧内存在每次方法调用结束之后自动弹出栈,自动释放内存,所以不需要垃圾回收对它处理。
<a name="TdJmw"></a>
### 2.2.2. 栈内存分配越大越好吗?

- 我们可以在程序运行的时候给栈分配指定的内存大小,通过参数:-Xss
- 栈划分的内存越大反而会让线程数变少,栈内存越大也就是方法递归调用的时候效率会高点,但是影响了线程数造成的影响更高,故而默认采取系统默认的栈内存大小就好。
<a name="xN07A"></a>
### 2.2.3. 方法区的局部变量是否线程安全?(局部变量的线程安全问题)

- **如果方法内局部变量没有逃离方法的作用范围那么它就是线程安全的;**
- **如果这个局部变量应用了对象并逃离了这个方法的作用范围,那么需要考虑线程安全问题。**
<a name="Mlreq"></a>
#### 2.2.3.1. 举例

1. 多线程打印方法区局部变量X变量的值,观察是否参数数值混乱。
```java
/**
 * 验证局部变量线程安全问题
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/9 18:21
 */
public class Demo2 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                method1();
            }
        };

        Thread thread1 = new Thread(runnable);
        thread1.start();


        Thread thread2 = new Thread(runnable);
        thread2.start();

    }

    private static void method1() {
        int x = 0 ;
        for (int i = 0; i < 100; i++) {
            x++ ;
        }
        System.out.println(x);
    }
}

运行结果:

100
100

结论:局部变量可以保证线程安全

  1. 把变量X改成static的变量,也用多线程打印,观察是否参数数字混乱。 ```java /**

    • 验证全局变量线程安全问题
    • @author : gnehcgnaw
    • @since : 2020/4/9 18:31 / public class Demo3 { /*

      • x 为全局变量 */ static int x = 0 ; public static void main(String[] args) { Runnable runnable = new Runnable() { @Override public void run() {

         method1();
        

        } };

        Thread thread1 = new Thread(runnable); thread1.start();

    Thread thread2 = new Thread(runnable);
    thread2.start();

}

private static void method1() {

    for (int i = 0; i < 100; i++) {
        x++ ;
    }
    System.out.println(x);
}

}

运行结果:
```java
100
200

结论:成员变量不能保证线程安全。

  1. 验证3 ```java /**

    • @author : gnehcgnaw
    • @since : 2020/4/9 18:36 */ public class Demo4 { public static void main(String[] args) { m1(); StringBuffer stringBuffer = new StringBuffer(); new Thread(()->{

         m2(stringBuffer);
      

      }).start();

      new Thread(()->{

         m2(stringBuffer);
      

      }).start();

      new Thread(()->{

         m2(new StringBuffer());
      

      }).start();

      new Thread(()->{

         m2(new StringBuffer());
      

      }).start(); }

      public static void m1(){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(1); stringBuffer.append(2); stringBuffer.append(3); System.out.println(stringBuffer.toString()); }

      /**

      • 有外来的参数
      • @param stringBuffer */ public static void m2(StringBuffer stringBuffer){ stringBuffer.append(1); stringBuffer.append(2); stringBuffer.append(3); System.out.println(stringBuffer.toString()); }

      /**

      • 逃离了方法的作用范围
      • @return */ public static StringBuffer m3(){ StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append(1); stringBuffer.append(2); stringBuffer.append(3); return stringBuffer; } }
<a name="npkud"></a>
## 2.3. 栈内存溢出
什么情况下导致栈内存溢出
<a name="EfmcO"></a>
### 2.3.1. 栈帧过多导致栈内存溢出
<a name="lLuh3"></a>
#### 2.3.1.1. 自己写的程序导致栈帧过多
递归终止条件不合适
```java
/**
 * 使用递归程序演示栈帧过多导致内存溢出
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/9 18:52
 */
public class Demo5 {
    public static void main(String[] args) {
        method1(1);
    }

    private static void method1(int i) {
        i++;
        System.out.println(i);
        method1(i);

    }
}

运行结果如下所示:(java.lang.StackOverflowError)10824次递归程序错误
image.png
修改虚拟机参数:-Xss256k,运行程序观察递归次数会不会变小?程序运行结果如下所示:(1872次出现错误)
image.png

2.3.1.2. 调用第三方程序导致栈帧过多

那有人说自己写的程序肯定不会出现这种情况,但是有时候第三包也会出现这样的问题,演示如下:

/**
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/9 19:02
 */
public class Demo6 {
    public static void main(String[] args) throws JsonProcessingException {

        Dept dept = new Dept();
        dept.setName("技术部");
        Emp emp1 = new Emp();
        emp1.setName("zhangsan");
        emp1.setDept(dept);

        Emp emp2 = new Emp();
        emp2.setName("zhangsan");
        emp2.setDept(dept);

        dept.setEmpList(Arrays.asList(emp1,emp2));

        ObjectMapper objectMapper = new ObjectMapper();
        System.out.println(objectMapper.writeValueAsString(emp1));
        System.out.println(objectMapper.writeValueAsString(dept));
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class Emp {
    private String name ;
    private Dept dept ;

}
@Data
@NoArgsConstructor
@AllArgsConstructor
class Dept {
    private String name ;

    private List<Emp> empList ;

}

运行结果如下所示:
image.png
解决方案:(在一方添加@JsonIgore注解)

    @JsonIgnore
    private Dept dept ;

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

(不太容易出现)

2.4. 线程运行诊断

2.4.1. 案例1:CPU占用过多

测试代码:

/**
 * 线程诊断:cpu占用高的问题
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/9 20:27
 */
public class Demo7 {
    public static void main(String[] args) {
        Thread t1 =  new Thread(()->{
            while (true){


            }
        },"t1");

        t1.start();

        Thread t2 =  new Thread(()->{
        },"t2");

        t2.start();

    }
}

诊断过程:

  1. 使用top命令定位是那个Java程序的CPU占用过高,获取到PID。

image.png

  1. 运行arthas,选择PID=4534的进程。

image.png

  1. 使用dashboard命令,可以看到当先进程下的线程情况,发现线程t1占用CPU过高。

image.png

  1. 使用thread pid 命令获取程序所在的类。

image.png

2.4.2. 案例2:程序运行长时间没有结果

(有可能多个线程之间发生了死锁)
测试代码:

import java.util.concurrent.TimeUnit;

/**
 * @author : <a href="mailto:gnehcgnaw@gmail.com">gnehcgnaw</a>
 * @since : 2020/4/9 20:58
 */
public class Demo8 {
    static A a = new A();
    static B b = new B();
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            synchronized (a){
                try {
                    TimeUnit.SECONDS.sleep(2000);
                    synchronized (b){
                        System.out.println(Thread.currentThread().getName());

                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        },"a").start();
        TimeUnit.SECONDS.sleep(1000);
        new Thread(()->{
            synchronized (b){
                synchronized (a){
                    System.out.println(Thread.currentThread().getName());

                }

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

    }
}

class A{}
class B{}

使用thread -b 检查是否有死锁
使用thread tid 查看线程
image.png
也可以使用jstack命令