JVM专题-虚拟机栈

Posted IT老刘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM专题-虚拟机栈相关的知识,希望对你有一定的参考价值。

1.定义

Java Virtual Machine Stacks (Java 虚拟机栈)

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

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;
	}
}

流程分析:

我们来打断点来Debug一下看一下方法执行的流程:

在控制台中可以看到,主类中的方法在进入虚拟机栈的时候,符合栈的特点

3.问题辨析

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

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

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

不是。因为物理内存是一定的,栈内存越大,可以支持更多的递归调用,但是可执行的线程数就会越少。

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

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


情况1:

情况2:如果你把int x变为static就会出现线程安全问题:

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

再来看一个案例:

/**
 * 局部变量的线程安全问题
 */
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()作用范围后,则不会存在线程安全
    }
}


小结:

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

3.4.栈内存溢出

Java.lang.stackOverflowError 栈内存溢出

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

如图所示,就是栈中栈帧过多的情况:

演示

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

执行次数:


当我们将虚拟机栈内存缩小到指定的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();
    }
}

  • top命令,查看是哪个进程占用CPU过高

  • ps H -eo pid, tid(线程id), %cpu | grep 刚才通过top查到的进程号 通过ps命令进一步查看是哪个线程占用CPU过高

  • jstack 进程id 通过查看进程中的线程的nid,刚才通过ps命令看到的tid来对比定位,注意jstack查找出的线程id是16进制的,需要转换

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专题-虚拟机栈的主要内容,如果未能解决你的问题,请参考以下文章

JVM技术专题深入研究JVM内存逃逸原理分析「研究篇」

jvm虚拟机栈的作用

Day326&327.虚拟机栈 -JVM

jvm内存区域之虚拟机栈

jvm内存区域之虚拟机栈

JVM05_虚拟机栈