JVM调优

JVM调优Q&A

为什么进行JVM调优?

调优的最终目的是为了应用程序使用最小的硬件消耗实现更大的吞吐量。针对垃圾收集器性能优化,减少GC频率、Full GC次数,实现虚拟机上应用使用更小的内存获取更大的吞吐量和更小的暂停时间和响应延迟

什么时候进行JVM调优?

  • 系统吞吐量与响应时间等性能参数不高或者下降,例如吞吐量下降、P90/P99响应增加等

  • 堆内存持续上涨、出现OOM

  • 频繁Full GC、GC耗时、停顿过长

  • 堆内存占用过高

调优具体是调什么?

内存分配+垃圾回收

  • 合理使用堆内存

  • GC高效回收垃圾对象

  • GC高效释放内存

本次调优实践的目标

  • 堆内存使用率<=70%

  • 老年代使用率<=70%

  • avg pause<=1s

  • Full GC次数0、avg pause interval>=24h

  • 创建更多的线程

调优原则是什么?

JVM自身拥有一定程度的容错和动态优化的能力

  • 优先原则:优先架构调优和代码调优,JVM是不得已的手段(大多数Java应用不需要JVM优化)

    产品-->架构-->代码-->数据库-->JVM

  • 观测性原则:发现问题解决问题,没有问题不找问题去解决

JVM调优主要步骤?

JVM调优步骤.png
  1. 监控JVM分析问题:评估必要性,通过GC日志查看内存使用、GC频率、GC耗时等情况,是否存在频繁GC或GC时间过长、内存占用持续上涨并长时间占用过高

    本案例通过SpringBoot Actuator+Micrometer+Prometheus+Grafana监控平台监控JVM性能指标

    通过GCEasy可视化分析GC日志

  2. 确定目标:内存占用、吞吐量响应等性能指标(法及回收)

  3. 制定方案:内存参数调整

  4. 验证方案:测试环境对比前后差异,确定目标是否实现,如未实现继续修改方案重新验证,控制变量单项优化循环多次

  5. 结果验收:灰度或仿真环境测试,如未实现目标继续重新修改方案,实现目标全量发布生效

JVM调优经典参数设置

  • -Xms、-Xmx、-Xss

  • -XX:+UseParallelGC、-XX:+UseParallelOldGC

  • -XX:+UseParNewGC、-XX:+UseConcMarkSweepGC

  • -XX:+UseG1GC

是否内存空间足够大,就不需要回收垃圾呢(JVM当内存占满触发垃圾回收)?

不可以

  1. 请求量加大或激增时,单个请求不论大小,空间占用都激增

  2. 物理层面:64位操作系统支持非常大内存,2~64 = 16384PB,但不是无限

  3. 虚拟机层面:不能设置无限内存

  4. 内存设置既不能设置太大,也不能设置太小,设置过大,一旦内存空间触发垃圾回收,大量对象需要扫描标记清除整理,每个操作耗时激增,导致程序停顿时间激增,设置太小需要频繁的垃圾回收并极容易OOM

GC日志

如何开启打印GC日志

  1. -XX:+PrintGCDetails 开启GC日志创建更详细的GC日志

  2. -XX:+PrintGCTimeStamps,-XX:+PrintGCDateStamps 开启GC时间提示

  3. -XX:+PrintHeapAtGC 打印堆的GC日志

  4. -Xloggc:./logs/gc.log 指定GC日志路径

GC日志解读

触发GC原因:

Allocation Failure:对象分配内存失败,年轻代空间不足
Metadata GC Threshold:元空间超阈值进行动态扩容
Ergonomics:译文是“人体工程学”,GC中的Ergonomics含义是负责自动调解 gc暂停时间和吞吐量之间平衡从而产生的GC

Young GC

//GC开始时间:GC开始时间相对于JVM启动时间间隔秒数
2021-05-18T14:31:11.340+0800: 2.340:
//Young GC还是Full GC(触发GC的原因)
[GC (Allocation Failure)
//垃圾收集器名称:垃圾收集前后新生代使用量(新生代总空间),垃圾收集前后堆使用量(堆总空间),GC持续时间
[PSYoungGen: 896512K->41519K(1045504K)] 896512K->41543K(3435008K), 0.0931965secs]
//GC线程消耗CPU时间,GC过程操作系统调用和系统等待时间,应用程序暂停时间
//用户态消耗的CPU时间、内核态消耗的 CPU事件和操作从开始到结束所经过的墙钟时间
[Times: user=0.14 sys=0.02, real=0.10 secs]

Full GC

//GC开始时间:GC开始时间相对于JVM启动时间间隔秒数
2021-05-19T14:46:07.367+0800: 1.562:
//Young GC还是Full GC(触发GC的原因)
[Full GC (Metadata GC Threshold)
//垃圾收集器名称:垃圾收集前后新生代使用量(新生代总空间)
[PSYoungGen: 18640K->0K(1835008K)]
//垃圾收集器名称:垃圾收集前后老年代使用量(老年代总空间),垃圾收集前后堆使用量(堆总空间),垃圾收集前后元空间使用量(元空间总空间),GC持续时间
[ParOldGen: 16K->18327K(1538048K)] 18656K->18327K(3373056K), [Metaspace:20401K->20398K(1069056K)], 0.0624559 secs]
//GC线程消耗CPU时间,GC过程操作系统调用和系统等待时间,应用程序暂停时间
//用户态消耗的CPU时间、内核态消耗的 CPU事件和操作从开始到结束所经过的墙钟时间
[Times: user=0.19 sys=0.00, real=0.06 secs]

可视化GC工具——GCEasy

JVM内存占用情况

gceasy-memory.png

其中包括年轻代、老年代、元空间、堆+元空间总和各指标的最大值和峰值及其图示

关键性能指标

gceasy-gctime.png
  1. 吞吐量:表示用户时间/用户时间+GC时间,百分比越高说明GC开销越低

  2. 平均GC暂停时间、最大GC暂停时间

可视化聚合结果

easygc-gcduration.png

GC持续时间,本图出现两次Full GC

GC统计信息

easygc-gcstatistic.png

GC原因

easygc-cause.png

Allocation Failure:对象分配内存失败,年轻代空间不足
Metadata GC Threshold:元空间超阈值进行动态扩容
Ergonomics:译文是“人体工程学”,GC中的Ergonomics含义是负责自动调解 gc暂停时间和吞吐量之间平衡从而产生的GC

JVM调优实践

调优前应用指标

启动命令:java -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:./logs/gc-default.log -jar hero_web-1.0-SNAPSHOT-thread800-nio-port9001-micrometer.jar -spring.application.location=/usr/local/hero/application-dev.yml

压测样本:请求延迟500ms+Tomcat max thread800+测试线程800*1000次

default1.png
default2.png
default3.png
default4.png

发现问题

  1. Meta Space使用峰值49M,分配空间1.04G,空间严重浪费

  2. Ergonomics可通过指定堆内存各分区避免动态调整

  3. 应用初始阶段,共三次Full GC,其中两次为Metadata GC Threshold,一次为Ergonomics。其中Metadata GC Threshold应该通过指定固定空间避免,Ergonomics为最小停顿时间和吞吐量平衡产生GC

制定方案

  1. 指定堆内存及元空间大小

    • 堆内存,建议扩大至3-4倍FullGC后的老年代空间,即121*(3-4)=(363-484),我们设置-Xms500m-Xmx500m

    • 新生代默认为堆内存1/3,建议设置1-1.5倍FullGC后老年代空间,即121*(1-1.5)-(121-181.5),我们设置-Xmn170m

    • 元空间,建议设置为2倍元空间峰值,即49*2=98,我们设置-XX:MetaspaceSize=100m

java -Xms500m -Xmx500m -Xmn170m -XX:MetaspaceSize=100m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:./logs/gc-bestheap-metaspace.log -jar hero_web-1.0-SNAPSHOT-thread800-nio-port9001-micrometer.jar -spring.application.location=/usr/local/hero/application-dev.yml
  1. 线程栈优化:1.8默认1M,将其设置-Xss512k,查看其性能指标

  2. 垃圾收集器优化:吞吐量优先ps+po

  3. 垃圾收集器优化:响应优先parNew+CMS

堆内存与元空间优化

启动命令:-Xms500m -Xmx500m -Xmn170m -XX:MetaspaceSize=100m

metaspace1.png
metaspace2.png
metaspace3.png
metaspace4.png

验证结果

  1. Full GC0次,GC平均时间与最大时间降低,Ergonomics与Metadata GC Threshold避免,通过指定内存容量
  2. 新生代内存由动态分配600M,指定后的160M,老年代120M到150M,小内存导致Young GC次数增加
  3. Allocation Failure因新生代内存不足,指定内存小于JVM默认动态分配,导致Young GC次数提升,GC时间增长,可以接受,也可通过分配更大内存来避免该情况但没有意义

线程堆栈优化

-Xms500m -Xmx500m -Xmn170m -XX:MetaspaceSize=100m -Xss512k

stack1.png
stack2.png
stack3.png
stack4.png

验证方案

  1. 各指标基本一致,有些微的提升,本方案也只是做个试验,工作中该方案没有任何意义
  2. 新生代老年代内存占用基本保持一致

垃圾收集器优化:吞吐量优先ps+po

之前优化方案未指定收集器,默认使用-XX:+UseParallelGC,即PS+PO

刻意减小内存,不然无法体现吞吐量优先,本次只与CMS对比,使用相同内存分配

-Xms256m -Xmx256m -Xmn125m -XX:MetaspaceSize=100m -Xss512k -XX:+UseParallelGC -XX:+UseParallelOldGC

ps-po1.png
ps-po2.png
ps-po3.png
ps-po4.png

验证方案

与CMS结果对比观看

垃圾收集器优化:响应优先parNew+CMS

-Xms256m -Xmx256m -Xmn125m -XX:MetaspaceSize=100m -Xss512k

cms1.png
cms2.png
cms3.png
cms4.png
cms5.png

验证方案

  1. PS+PO出现元空间扩容,分配1G

  2. PS+PO吞吐量高于ParNew+CMS,PS+POGC时间高于ParNew+CMS

  3. CMS并发标记+重新标记停止时间350ms,远小于PO的近2s

  4. ParNew暂停时间13.3s,高于PS的10.23s

  5. ParNew+CMS GC时间高于PS+PO,并发处理GC耗时部分,属于响应优先

  6. 同样内存分配下,PS+PO仍会触发Ergonomics(原因不详)

  7. 新生代老年代内存占用基本保持一致

G1验证

启动命令:-XX:+UseG1GC

g1.png
g12.png

验证结论

  1. G1内存最小2G,与之前性能无法对比

  2. G1吞吐量非常高,响应延迟也很低,GC时间短

JVM调优代码实践

内存溢出定位分析

模拟内存溢出代码,使用VM参数:-Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError

/**
 * 1.7:-XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m
 * 1.8:-XX:PermSize=8m -XX:MaxPermSize=8m -Xmx16m
 * -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError
 */
public class StringOomMock {
    static String  base = "string";
    public static void main(String[] args) {
        List list = new ArrayList();
        for (int i=0;i< Integer.MAX_VALUE;i++){
            String str = base + base;
            base = str;
            list.add(str.intern());
        }
    }
}
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid24616.hprof ...
Heap dump file created [6356499 bytes in 0.016 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:3332)
    at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
    at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
    at java.lang.StringBuilder.append(StringBuilder.java:136)
    at com.zhaoccf.study.jvm.memory.StringOomMock.main(StringOomMock.java:16)

使用MAT分析dump文件

  1. MAT根据dump文件分析可能的大对象信息汇总,提供历史引用信息、支配树(引用树)、最昂贵对象信息、重复的classes等汇总信息
mat总览.jpg
  1. MAT分析怀疑的大对象信息列表,并给出了可能的问题选项及其对象详情
mat分析可能的对象.jpg
  1. MAT怀疑对象的详情信息,包括引用、本身内存占用(shallow heap)及该对象释放和因其释放而释放的对象内存占用(retained heap)
mat怀疑对象引用详细信息.jpg
  1. 可以查看该对象的上游引用及其引用的对象的内存占用情况
mat怀疑对象详细信息及引用对象.jpg

死锁检测

模拟死锁代码,启动2个线程,Thread1拿到了obj1锁,准备去拿obj2锁时,obj2已经被Thread2锁定,发生了死锁

public class DeadLockTest {
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (obj1) {
                System.out.println("Thread1 拿到了obj1的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (obj2) {
                    System.out.println("Thread1 拿到了obj2的锁");
                }
            }
        }).start();
        new Thread(() -> {
            synchronized (obj2) {
                System.out.println("Thread1 拿到了obj2的锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (obj1) {
                    System.out.println("Thread1 拿到了obj1的锁");
                }
            }
        }).start();
    }
}

jstack分析

jstack 21451
2022-09-12 17:46:41
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.261-b12 mixed mode):

"Attach Listener" #12 daemon prio=9 os_prio=0 tid=0x00007f1668001000 nid=0x585c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"DestroyJavaVM" #11 prio=5 os_prio=0 tid=0x00007f16a8009800 nid=0x53cc waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #10 prio=5 os_prio=0 tid=0x00007f16a8193000 nid=0x53db waiting for monitor entry [0x00007f1681d15000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockTest.lambda$main$1(DeadLockTest.java:28)
        - waiting to lock <0x00000000d745cff8> (a java.lang.Object)
        - locked <0x00000000d745d008> (a java.lang.Object)
        at DeadLockTest$$Lambda$2/303563356.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #9 prio=5 os_prio=0 tid=0x00007f16a8191800 nid=0x53da waiting for monitor entry [0x00007f1681e16000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockTest.lambda$main$0(DeadLockTest.java:15)
        - waiting to lock <0x00000000d745d008> (a java.lang.Object)
        - locked <0x00000000d745cff8> (a java.lang.Object)
        at DeadLockTest$$Lambda$1/471910020.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007f16a80c8800 nid=0x53d8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007f16a80bd800 nid=0x53d7 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f16a80bb800 nid=0x53d6 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread0" #5 daemon prio=9 os_prio=0 tid=0x00007f16a80b8800 nid=0x53d5 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Signal Dispatcher" #4 daemon prio=9 os_prio=0 tid=0x00007f16a80b7000 nid=0x53d4 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Finalizer" #3 daemon prio=8 os_prio=0 tid=0x00007f16a8086000 nid=0x53d3 in Object.wait() [0x00007f168251d000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d7408ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
        - locked <0x00000000d7408ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)

"Reference Handler" #2 daemon prio=10 os_prio=0 tid=0x00007f16a8081800 nid=0x53d2 in Object.wait() [0x00007f168261e000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x00000000d7406c00> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x00000000d7406c00> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)

"VM Thread" os_prio=0 tid=0x00007f16a8078000 nid=0x53d1 runnable 

"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f16a801e800 nid=0x53cd runnable 

"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f16a8020800 nid=0x53ce runnable 

"GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f16a8022800 nid=0x53cf runnable 

"GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f16a8024000 nid=0x53d0 runnable 

"VM Periodic Task Thread" os_prio=0 tid=0x00007f16a80cb800 nid=0x53d9 waiting on condition 

JNI global references: 310


Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f1674006218 (object 0x00000000d745cff8, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007f1674003988 (object 0x00000000d745d008, a java.lang.Object),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at DeadLockTest.lambda$main$1(DeadLockTest.java:28)
        - waiting to lock <0x00000000d745cff8> (a java.lang.Object)
        - locked <0x00000000d745d008> (a java.lang.Object)
        at DeadLockTest$$Lambda$2/303563356.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)
"Thread-0":
        at DeadLockTest.lambda$main$0(DeadLockTest.java:15)
        - waiting to lock <0x00000000d745d008> (a java.lang.Object)
        - locked <0x00000000d745cff8> (a java.lang.Object)
        at DeadLockTest$$Lambda$1/471910020.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

Found 1 deadlock.
[root@ecs-zhaocc-01 ~]# jstack 21451|grep BLOCKED -A15 --color
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockTest.lambda$main$1(DeadLockTest.java:28)
        - waiting to lock <0x00000000d745cff8> (a java.lang.Object)
        - locked <0x00000000d745d008> (a java.lang.Object)
        at DeadLockTest$$Lambda$2/303563356.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Thread-0" #9 prio=5 os_prio=0 tid=0x00007f16a8191800 nid=0x53da waiting for monitor entry [0x00007f1681e16000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at DeadLockTest.lambda$main$0(DeadLockTest.java:15)
        - waiting to lock <0x00000000d745d008> (a java.lang.Object)
        - locked <0x00000000d745cff8> (a java.lang.Object)
        at DeadLockTest$$Lambda$1/471910020.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:748)

"Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007f16a80c8800 nid=0x53d8 runnable [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C1 CompilerThread2" #7 daemon prio=9 os_prio=0 tid=0x00007f16a80bd800 nid=0x53d7 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"C2 CompilerThread1" #6 daemon prio=9 os_prio=0 tid=0x00007f16a80bb800 nid=0x53d6 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

Thread0获取了 <0x00000000d745cff8> 的锁,等待获取 <0x00000000d745d008>这个锁
Thread1获取了 <0x00000000d745d008> 的锁,等待获取 <0x00000000d745cff8>这个锁
由此可见,发生了死锁。

Arthas分析

[arthas@21451]$ thread -b
"Thread-1" Id=10 BLOCKED on java.lang.Object@2824de1f owned by "Thread-0" Id=9
    at DeadLockTest.lambda$main$1(DeadLockTest.java:28)
    -  blocked on java.lang.Object@2824de1f
    -  locked java.lang.Object@63f07679 <---- but blocks 1 other threads!
    at DeadLockTest$$Lambda$2/303563356.run(Unknown Source)
    at java.lang.Thread.run(Thread.java:748)

你可能感兴趣的:(JVM调优)