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

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

3.虚拟机栈

3.1 虚拟机栈的介绍

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

e75b34184b364c63b92406046132090f.png

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

8afeb8a7b734440484de647f8d4ee252.png

💡 思考

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

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

Q2:栈内存越大越好吗?

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

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

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

f5c0143f0dc94da894ed9f7e103608c4.png

94bca2daf8f648ac965aea173ba53c96.jpg

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

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

先看这个例子。

// 多个线程同时执行
static void m1() {
        int x = 0;
        for (int j = 0; j < 500; j++) {
            x++;
        }
}

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

4c723db4cc0d449985b8b3c0e3312f3a.png

再看看下面的例子。

 static void m2() {
     StringBuilder sb = new StringBuilder();
     sb.append("a");
     sb.append("b");
     sb.append("c");
}

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

 static void m3(StringBuilder sb ) {
        sb = new StringBuilder();
        sb.append("a");
        sb.append("b");
        sb.append("c");
}

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

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

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

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

导致栈内存溢出的情况:

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

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

public class Demo02 {
    private static int count;
    public static void main(String[] args) {
        m1();
    }
    static void m1() {
        count ++;
        m1();
    }
}

4aab228522ef4dc188e3ebe2657a4d68.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;
    }
}

出现Infinite recursion (StackOverflowError)

网络异常,图片无法展示
|

解决方法:添加@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;
    }
}

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

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

nohub java Demo04 &

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


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

752b5d975b544e01a2e87ae7bbfe9651.png

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

image.png

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


image.png

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

while(true) {
 }

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

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


a63df57a5d6c4987aa1f5024bcbd08dd.png


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

e26604ef9faf4acf81b48cededbe036a.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.

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

5.本地方法栈


7ef4c2eea5d241f3931b2aa0a08277d5.png

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

相关文章
|
1月前
|
存储 Java 开发者
浅析JVM方法解析、创建和链接
上一篇文章《你知道Java类是如何被加载的吗?》分析了HotSpot是如何加载Java类的,本文再来分析下Hotspot又是如何解析、创建和链接类方法的。
|
1月前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
2月前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
44 1
|
2月前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
2月前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
75 1
|
2月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
2月前
|
监控 Java Spring
JVM如何监控某个方法的入参和相应结果?
JVM如何监控某个方法的入参和相应结果?
48 0
|
2月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
515 1
|
27天前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
|
1月前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。