• 【JVM调优实战100例】02——虚拟机栈与本地方法栈调优五例


    前 言
    🍉 作者简介:半旧518,长跑型选手,立志坚持写10年博客,专注于java后端
    ☕专栏简介:实战案例驱动介绍JVM知识,教你用JVM排除故障、评估代码、优化性能
    🌰 文章简介:介绍虚拟机栈与本地方法栈、教你排查5个常见的JVM虚拟机栈案例实战

    3.虚拟机栈

    3.1 虚拟机栈的介绍

    栈:线程运行时需要的内存空间,一个栈中包含多个栈帧,栈帧是每个方法运行时需要的内存,一次方法调用就是一个栈帧。栈帧主要是用来存储局部变量,参数与返回地址(结束该方法后执行方法的地址)的。调用一个方法时,方法的栈帧入栈,当该方法执行结束,对应的栈帧(Frame)就会出栈。另外每个线程只能有一个活动栈帧,来对应当前正在执行的方法。
    在这里插入图片描述

    使用idea可以调试获取虚拟机栈信息。左下角的Frames就对应虚拟机栈。

    在这里插入图片描述

    💡 思考

    Q1:垃圾回收是否涉及栈内存

    A1:垃圾回收不会涉及栈内存,因为栈的栈帧会随着方法调用而入栈,随着方法结束而出栈,无需进行垃圾回收。

    Q2:栈内存越大越好吗?

    A2:栈的大小可以进行设置。

    线程栈越大则可以进行嵌套调用的方法层级越多,但是需要在合理区间,不是越大越好。因为计算机的物理内存是有限的,线程中栈的大小设置的越大,可以容纳的线程数就会越少(每个线程都有自己的栈)。一般采用系统默认的栈内存大小即可。

    下图展示了设置栈大小的方法。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2b5iHGgI-1656678163012)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/001.png)]

    在这里插入图片描述

    3.2 方法局部变量线程安全问题

    局部变量是方法栈的私有变量,那么方法内的局部变量是不是一定是线程安全的呢?

    先看这个例子。

    // 多个线程同时执行
    static void m1() {
            int x = 0;
            for (int j = 0; j < 500; j++) {
                x++;
            }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面的例子是不会有线程安全问题的。因为每个线程都有独立的栈帧,存储独立的x。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0p3I5ZHq-1656678163013)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/2.png)]

    再看看下面的例子。

     static void m2() {
         StringBuilder sb = new StringBuilder();
         sb.append("a");
         sb.append("b");
         sb.append("c");
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    答案依旧不会有安全问题,理由与上面的例子一样。接着看下下面的例子。

     static void m3(StringBuilder sb ) {
            sb = new StringBuilder();
            sb.append("a");
            sb.append("b");
            sb.append("c");
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    上面例子其实是线程不安全的。因为sb 不是线程私有的。

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

    • 如果方法内的局部变量没有逃离方法的作用范围,则是安全的。
    • 如果是基本数据类型,则是安全的。
    • 如果是对象类型数据,并且逃离了方法的作用范围,则线程不安全。参考代码demo1,不同线程栈的变量中存放的地址不会彼此干扰,但同一地址的值可以被不同的线程所修改。

    3.3 虚拟机栈的内存溢出问题

    导致栈内存溢出的情况:

    • 入栈栈帧过多,如方法递归次数过多。
    • 栈帧过大,这种情况很少出现,因为默认的栈帧大小是1M,可以存放空间很充足。

    下面就是一个栈内存溢出的例子。

    public class Demo02 {
        private static int count;
    
        public static void main(String[] args) {
            m1();
        }
    
        static void m1() {
            count ++;
            m1();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySVgEBAZ-1656678163013)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/3.png)]

    值得注意的是,有时候并不是我们自己写的代码导致了栈的内存溢出问题,而是错误使用第三方库的代码时导致了内存溢出问题。

    /**
     * json 数据转换
     */
    public class Demo03 {
    
        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;
        @JsonIgnore
        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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    出现Infinite recursion (StackOverflowError)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D0nhUdKn-1656678163014)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/4.png)]

    解决方法:添加@JsonIgnore注解。

    class Emp {
        private String name;
        @JsonIgnore
        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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    3.4 虚拟机栈的cpu占用问题

    下面分析两个栈相关的案例。

    编译运行下面代码。

    /**
     * 演示 cpu 占用过高
     */
    public class Demo04 {
    
        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();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    linux下使用nohub让进程在后台运行,将直接返回线程id。

    nohub java Demo04 &
    
    • 1

    使用top来显示cpu被进程占用的情况(笔者环境是windows,直接用的任务管理器,后不再赘述)。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-057EdCHr-1656678163014)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/5.png)]

    定位到占用过高cpu的进程后,使用ps H -eo pid tid %cpu | grep xxx(进程id)来查看具体是哪个线程导致的问题。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40Z3VkwN-1656678163014)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/6.png)]

    最后使用jstack xxx(进程id)查看进程所有线程对应的id及引起问题的源码行数。注意使用第二步得到线程编号是十进制,而jstack中的线程编号是16进制,需要进行必要的进制换算。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-unEamyNf-1656678163015)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/image-20220629205806512.png)]

    32655换算成16进制就是7f99,因此有问题的线程就是下面的线程。其线程状态是runnable,说明它一直在运行,占用了cpu。并且还可以根据堆栈信息定位到具体的代码行数。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XA2dCBbr-1656678163015)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/image-20220629210212804.png)]

    对应到源代码,我们就排查出了导致cpu占用过高的原因了。

     while(true) {
    
     }
    
    • 1
    • 2
    • 3

    3.5 线程死锁的排查

    编写如下代码。

    /**
     * 演示线程死锁
     */
    class A{};
    class B{};
    public class Demo05 {
        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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    linux下使用nohub让进程在后台运行,将直接返回线程id。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fT5eRoSN-1656678163019)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/image-20220630203443201.png)]

    windows上可以直接使用java运行,在任务管理器中找到该进程,可以看到进行id是15288。(linux环境有很多开发命令,笔者环境是windows,结合了git batsh使用linux的部分命令,后不再赘述)

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GQNSSZnU-1656678163019)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/image-20220630203131063.png)]

    执行jsatck命令,可以看到如下输出

    F:\资料 解密JVM\代码\jvm\src\cn\itcast\jvm\t1\stack>jstack 15288
    2022-06-30 20:30:08
    Full thread dump Java HotSpot(TM) Client VM (25.301-b09 mixed mode):
    
    ...
    
    Found one Java-level deadlock:
    =============================
    "Thread-1":
      waiting to lock monitor 0x01199894 (object 0x04e9fb40, a A),
      which is held by "Thread-0"
    "Thread-0":
      waiting to lock monitor 0x0119c1b4 (object 0x04ea0c28, a B),
      which is held by "Thread-1"
    
    Java stack information for the threads listed above:
    ===================================================
    "Thread-1":
            at Demo05.lambda$main$1(Demo05.java:28)
            - waiting to lock <0x04e9fb40> (a A)
            - locked <0x04ea0c28> (a B)
            at Demo05$$Lambda$2/1503869.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:748)
    "Thread-0":
            at Demo05.lambda$main$0(Demo05.java:20)
            - waiting to lock <0x04ea0c28> (a B)
            - locked <0x04e9fb40> (a A)
            at Demo05$$Lambda$1/28568555.run(Unknown Source)
            at java.lang.Thread.run(Thread.java:748)
    
    Found 1 deadlock.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    可以很清楚看到死锁信息被定位了,在Demo05.java:28,20行出现了死锁。再去代码处分析,发现线程1,2出现了互锁。并且这个互酸信息其实也被打印出来了,Thread-1,拥有B等待A,Thread-2,拥有A等待B。

    5.本地方法栈

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o64VBVpG-1656678163019)(F:/%E5%8D%9A%E5%AE%A2%E5%9B%BE%E7%89%87/jvm/image-20220630204811251.png)]

    本地方法是非java语言(c/c++)编写的直接与计算机操作系统底层API交互的方法,java虚拟机在调用本地方法时,通过本地方法栈给本地方法提供内存空间。

  • 相关阅读:
    【Lipschitz】基于matlab的Lipschitz李氏指数仿真
    STM32-串口通信波特率计算以及寄存器的配置详解
    适合家电和消费类应用R7F101GEE4CNP、R7F101GEG4CNP、R7F101GEG3CNP、R7F101GEE3CNP新一代RL78通用微控制器
    Win10 如何删除系统盘大文件hiberfil.sys
    关于ASCII码的了解
    Redis:redis为什么会分16个库、与数据库关联时是先更新数据库还是先更新缓存、大多数情况为什么删除而不是更新缓存
    Zookeeper部署运行_服务管理
    Thread常用方法介绍
    保证金服务数据一致性问题-大数据解决方案
    平均年薪20W,自动化测试工程师这么吃香?
  • 原文地址:https://blog.csdn.net/qq_41708993/article/details/125565406