JVM常见问题与调优

发布于:2025-04-12 ⋅ 阅读:(38) ⋅ 点赞:(0)

目录

一、内存管理问题

1、内存泄漏(Memory Leak)

2、内存溢出(OOM, OutOfMemoryError)

2.1 堆内存溢出(OutOfMemoryError: Java heap space)

2.2 元空间溢出(OutOfMemoryError: Metaspace)

2.3 栈溢出(StackOverflowError)或 栈内存不足(OutOfMemoryError: Unable to create native thread)

二、垃圾回收(GC)问题

1、频繁垃圾回收

1.1 频繁YoungGC

1.2 频繁FullGC

2、GC算法选择不当

三、类加载问题

1、ClassNotFoundException

2、NoClassDefFoundError

3、依赖冲突:NoSuchMethodError / NoSuchFieldError

4、元空间溢出

5、类重复加载

四、线程和锁问题

1、死锁(Deadlock)

2、线程数过多

3、CPU飙升(热点代码)

五、其他问题

1、未启用压缩指针

2、JIT编译问题


一、内存管理问题

1、内存泄漏(Memory Leak)

表现:对象不再使用但无法被GC回收,导致内存逐渐耗尽。
原因:静态集合持有对象、未关闭资源(如数据库连接)、监听器未注销等。
排查工具:jmap生成堆转储,通过MAT(Memory Analyzer Tool)分析对象引用链。

jps -l    # 显示进程id、主类全名或 JAR 路径
jmap -dump:live,format=b,file=heap.hprof <pid>  # 生成存活对象的堆转储文件

MAT的使用参照:MAT

        通过Dominator Tree,显示占用内存最多的对象及其引用关系,查找未释放的引用

2、内存溢出(OOM, OutOfMemoryError)

2.1 堆内存溢出(OutOfMemoryError: Java heap space)

本质:对象占用的堆内存超过了 JVM 最大堆容量(-Xmx)

原因:内存泄漏(代码逻辑问题导致对象无法被GC回收);数据规模过大(没有分页或分批处理);JVM堆设计不合理(-Xmx最大堆内存设计过小);高并发场景(对象创建超过GC回收,频繁创建大对象,未复用线程池资源)

特征:抛出异常java.lang.OutOfMemoryError: Java heap space;FullGC频繁触发且快速占满(通过 jstat -gcutil 观察);堆内存使用率持续高位(接近100%,老年代空间不足);服务响应变慢甚至进程崩溃。

解决

1> 应急处理

        临时扩容:增大 JVM 堆参数(-Xmx 和 -Xms),但需结合物理机内存。
        重启服务:快速恢复业务,但需后续根因分析。

2> 原因及对应问题解决

        内存泄漏:生成堆转储文件,用MAT定位泄漏对象

        数据规模过大:避免全量加载,分页处理数据

        缓存设计:限制缓存容量和过期时间,使用分布式缓存替代本地缓存

        代码设计:优化大对象创建(如复用对象、使用对象池);避免在循环中创建无意义临时对象(如字符串拼接改用StringBuilder)

        JVM参数:根据业务负载调整-Xmx 和-Xms(建议初始值=最大值,避免动态扩容开销);合理设置新生代与老年代比例(增大新生代比例(-XX:NewRatio=2),避免过早晋升对象到老年代;调整Survivor区大小(-XX:SurvivorRatio=8),减少对象复制次数);限制大对象分配:通过-XX:PretenureSizeThreshold直接分配大对象到老年代,减少新生代压力

监控

1> 在OOM时自动生成Heap Dump

在启动应用时添加参数:

java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/logs/heapdumps/-%p-%t.hprof \
     -XX:OnOutOfMemoryError="sh /scripts/restart_service.sh" \
     -jar your-app.jar

-XX:+HeapDumpOnOutOfMemoryError
        启用 OOM 时自动生成堆转储的功能
-XX:HeapDumpPath=/path/to/dump/directory/-%p-%t.hprof
        指定堆转储文件的保存路径(需确保目录存在且有写入权限),按进程ID和时间戳生成唯一文件名
-XX:OnOutOfMemoryError="sh /scripts/restart_service.sh"
        在 OOM 时触发自定义命令(如发送告警、重启服务)

2> 设置JVM的告警阈值并生成堆转储文件

使用jstat查看堆内存

# 实时监控堆内存使用率(间隔 1 秒)
jstat -gcutil <PID> 1000

# 输出示例
S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
0.00  99.80  10.00  80.50  95.20  92.10   15     0.250    3     0.450    0.700

编写 Shell 脚本触发告警并生成堆转储文件:老年代超过80%

#!/bin/bash
PID=$(jps | grep YourAppName | awk '{print $1}')
THRESHOLD=80
DUMP_DIR="/data/heapdumps"

while true; do
  # 获取老年代使用率
  O_USAGE=$(jstat -gcutil $PID | tail -1 | awk '{print $4}')
  
  # 判断是否超过阈值
  if (( $(echo "$O_USAGE > $THRESHOLD" | bc -l) )); then
    echo "警告:堆内存使用率超过 ${THRESHOLD}% (当前: ${O_USAGE}%)" | mail -s "JVM 告警" admin@example.com
   # 生成堆转储文件(按时间戳命名)
    DUMP_FILE="$DUMP_DIR/heapdump_$(date +%Y%m%d%H%M%S).hprof"
    jmap -dump:live,format=b,file=$DUMP_FILE $PID
    echo "堆转储已生成: $DUMP_FILE"
  fi
  
  sleep 60  # 每分钟检查一次
done

3> 定期分析历史内存使用数据,调整 -Xmx 和 -Xms

2.2 元空间溢出(OutOfMemoryError: Metaspace)

原因:动态生成大量类(类加载过多)

        例如:Spring AOP默认使用CGLIB生成代理类,若频繁生成且未回收,导致元空间膨胀;某些ORM框架(如Hibernate)动态生成代理类未回收

问题排查

1> 查看元空间使用情况

jcmd <PID> VM.metaspace  # 查看元空间统计信息
jstat -gcmetacapacity <PID>  # 监控元空间容量变化

可以使用JConsole查看元空间使用情况

2> 启用JVM参数

-XX:NativeMemoryTracking=summary -XX:+UnlockDiagnosticVMOptions

-XX:+UnlockDiagnosticVMOptions
作用:解锁JVM的诊断选项。许多高级诊断和调试功能(如内存跟踪、JVM内部状态监控等)默认被锁定,此参数允许启用这些功能。
-XX:NativeMemoryTracking=summary
作用:启用本地内存跟踪(NMT)的概要模式,监控JVM内部组件的本地(非堆)内存使用情况。

通过jcmd <PID> VM.native_memory summary命令查看实时内存报告,分析本地内存分配

jcmd <PID> VM.native_memory summary

3> 生成和分析堆转储

生成堆转储:

jmap -dump:live,format=b,file=heapdump.hprof <PID>  # 手动生成
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path  # OOM时自动生成

通过分析工具MAT检查ClassLoader对象和加载的类数量

4> 类加载器泄漏定位

在堆转储中搜索ClassLoader实例,检查是否残留未回收的自定义类加载器

重点关注以下场景:

  • Web应用卸载后仍被引用(如静态变量持有ClassLoader)。

  • 动态代理类未释放(如CGLIB生成的$$EnhancerByCGLIB$$类)。

  • 线程上下文类加载器(TCCL)未重置

解决方案

1> 限制元空间大小:显式设置元空间上限,避免无限增长

-XX:MaxMetaspaceSize=256m  # 根据应用需求调整

提高初始元空间大小(-XX:MetaspaceSize),减少动态扩容次数

2> 代码方面

避免静态变量持有ClassLoader或动态生成的类

确保自定义类加载器的生命周期管理(如Web应用关闭时调用close()方法)

优化反射、动态代理、字节码增强(如ASM)的使用频率,避免滥用反射和动态代理(如限制CGLIB生成类数量)

缓存重复使用的动态类(如使用ClassValue或第三方缓存库)

3> 框架设置

Spring:优先使用JDK动态代理(proxyTargetClass=false)代替CGLIB

Tomcat:检查Context.reload逻辑,确保旧类加载器被GC回收

2.3 栈溢出(StackOverflowError)或 栈内存不足(OutOfMemoryError: Unable to create native thread)

原因:递归过深或栈帧过大(单个线程的栈大小默认1MB,-Xss1m)

问题排查

1> 定位栈溢出位置

异常堆栈的日志会显示调用链,如:

Exception in thread "main" java.lang.StackOverflowError
    at com.example.MyClass.recursiveMethod(MyClass.java:10)
    at com.example.MyClass.recursiveMethod(MyClass.java:10)
    ...(重复上千次)

通过堆栈末尾的重复行快速定位问题方法

添加 -XX:+PrintGCDetails -XX:+PrintStackAtOverflowError,在溢出时打印更详细的栈信息

2> 线程栈分析

使用 jstack 查看线程栈:

jstack <PID> > thread_dump.txt

检查所有线程的调用栈,寻找异常调用链。

3> 代码静态分析

  • 检查递归方法:确认递归终止条件是否正确。

  • 检查循环调用:查找方法间相互调用的逻辑。

  • 检查大局部变量:避免在方法内定义过大的数组或对象。

解决方案

1> 调整 JVM 参数:增大线程栈大小

-Xss2m  # 将栈大小调整为 2MB

注意

  • 增大栈空间可能掩盖代码问题,需优先优化代码逻辑。

  • 高并发场景下,过大的栈空间会导致总内存消耗激增(总内存 ≈ 线程数 × 栈大小)。

2> 优化代码逻辑

递归改写为迭代;减少方法调用深度;避免大局部变量(将大对象移到堆内存中,如通过 new 创建而非局部定义)

3> 控制线程数量

  • 限制线程池大小(如使用 ThreadPoolExecutor 时合理设置核心线程数)。

  • 避免无限制创建线程(如使用异步框架需配置并发度)。

二、垃圾回收(GC)问题

1、频繁垃圾回收

1.1 频繁YoungGC

现象:Young GC触发频率高(如每秒多次),且每次回收后Eden区很快再次填满

原因:短时间创建大量短生命周期对象;年轻代-Xmn设置国小,无法容纳正常对象分配;内存泄漏,某些短命对象被意外长期引用导致无法被回收

问题定位

1> 确认Young GC频率

查看GC日志(需启用日志记录):

-Xlog:gc*,gc+heap=debug:file=gc.log:time,uptime:filecount=10,filesize=10M

关键信息:GC触发间隔、每次回收后的Eden/Survivor使用量。

GC日志:[GC (Allocation Failure) [PSYoungGen: ...] 频繁出现。

2> 使用jstat监控内存变化

jstat -gcutil <PID> 1000  # 每秒输出一次各区域使用率

关注列:E(Eden使用率)、S0/S1(Survivor区)、YGC(Young GC次数)

3> 分析对象分配热点

使用Profiler工具(如async-profiler、Arthas的profiler)生成分配火焰图,定位高频分配代码

Arthas命令:

profiler start -e alloc -d 60  # 监控60秒内的对象分配

4> 检查Survivor区对象晋升

GC日志分析:观察每次Young GC后进入老年代的对象大小(-XX:+PrintTenuringDistribution

-XX:+PrintTenuringDistribution  # 输出年龄分布信息

解决方案:

1> 减少临时对象创建(如重用对象、使用基本类型替代包装类),避免在循环内频繁创建大对象(如JSON解析结果)

2> 调整年轻代大小

增大Eden区:通过调整-XX:SurvivorRatio或直接设置-Xmn(年轻代总大小)

-XX:SurvivorRatio=4  # Eden:S0:S1=4:1:1(增大Eden)
-Xmn2g              # 年轻代设为2G(根据堆总大小调整)

注意:如果survivor区过小,Young GC后存活的对象无法进入Survivor区,会直接晋升老年代,频繁触发Full GC

3> 避免内存泄漏

  • 检查代码:确保集合缓存、监听器、线程局部变量(ThreadLocal)及时清理。

  • 工具验证:通过堆转储分析Eden区中“本应回收”的对象为何存活。

    1.2 频繁FullGC

    现象:Gang Worker线程CPU高,伴随Full GC日志

    原因:内存泄漏(FullGC后释放内存极少);堆内存分配不合理(年轻代过小导致对象过早晋升老年代、老年代过小导致频繁GC);大对象阈值设置不当,直接分配到老年代;代码显式调用System.gc();元空间(Metaspace)溢出(类加载元数据占用过多内存,间接影响堆内存稳定性);GC算法选择不当(G1 Humongous对象分配:大对象分配导致Region碎片化)

    问题定位

    1> 分析GC日志

    • 启用GC日志:

    -Xlog:gc*,gc+heap=debug:file=gc.log:time:filecount=10,filesize=100M

    关键指标:

    • Full GC频率、耗时、回收前后内存变化。

    • 老年代占用率是否持续增长(内存泄漏特征)。

    2> 监控工具

    jstat实时监控:

    jstat -gcutil <pid> 1000  # 每秒输出一次内存区域利用率

      关注 FGC(Full GC次数)、FGCT(Full GC总耗时)、OU(老年代使用率)

      jmap生成堆转储

      jmap -dump:live,format=b,file=heap.hprof <pid>

      3>堆转储分析:MAT

      步骤:

      • 查看支配树(Dominator Tree)找到占用内存最大的对象。
      • 检查集合类(如HashMapArrayList)是否持有大量无用对象。
      • 追踪对象引用链,定位泄漏代码位置。

      4> 检查显式GC调用

      代码搜索System.gc()Runtime.getRuntime().gc()

      5> 元空间监控

      • 使用jstat -gcutil查看MU(Metaspace使用率)。

      • 检查是否动态生成大量类(如反射、CGLIB代理)。

      解决

      1> 调整堆内存分布

      调整大对象阈值:避免频繁分配大对象(如拆分大文件读取为分块处理)

      -XX:PretenureSizeThreshold=1M  # 对象超过1MB直接进入老年代

      优化分代比例:

      • 年轻代与老年代比例(-XX:NewRatio=2,即老年代占2/3)。

      -Xmx4g -Xms4g  # 增大堆总大小
      -XX:NewRatio=2  # 老年代与新生代比例为2:1
      • 增大Survivor区,避免年轻代过小导致对象过早晋升老年代(-XX:SurvivorRatio=8,Eden:Survivor=8:1:1)。

      • 调整晋升阈值

      -XX:MaxTenuringThreshold=15  # 提高对象在Survivor区的存活次数阈值(默认15)

      2> 禁用显式GC

      -XX:+DisableExplicitGC  # 阻止System.gc()触发Full GC

        3> 修复内存泄漏

        4> 调整GC算法

        G1 GC调优(适用于大堆内存):

        -XX:+UseG1GC
        -XX:MaxGCPauseMillis=200     # 目标最大停顿时间
        -XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用阈值

        切换为ZGC/Shenandoah(超低延迟场景):

        -XX:+UseZGC                  # JDK 11+
        -XX:+UseShenandoahGC         # OpenJDK 12+

        5> 元空间优化

        限制元空间大小:

        -XX:MaxMetaspaceSize=256m

        减少动态类生成:避免滥用反射或动态代理。

        2、GC算法选择不当

        GC算法的选择

        GC 算法 适用场景 核心参数
        Parallel GC 高吞吐量,容忍较长停顿(后台计算) -XX:+UseParallelGC
        CMS 低停顿,老年代回收(已废弃) -XX:+UseConcMarkSweepGC
        G1 GC 平衡吞吐量和停顿时间(默认 JDK 9+) -XX:+UseG1GC
        ZGC 超低停顿(10ms 以下),大堆内存 -XX:+UseZGC
        Shenandoah 低停顿,与 ZGC 类似(非 Oracle JDK) -XX:+UseShenandoahGC

        三、类加载问题

        1、ClassNotFoundException

        现象:运行时找不到指定类

        原因:

        • 类路径(Classpath)未正确配置,缺少依赖的 JAR 包。

        • 动态加载类时路径错误(如反射加载类名拼写错误)。

        • 类加载器未正确传递委托(如自定义类加载器未遵循双亲委派)。

        问题定位:类加载日志、检查类路径

        1> 启用类加载日志:添加 JVM 参数,追踪类加载过程,观察日志中缺失的类或重复加载的类

        -verbose:class  # 打印加载的类信息
        -XX:+TraceClassLoading  # 更详细的类加载日志(JDK 8+)

        2> 检查类路径:确认 -classpath 或 CLASSPATH 环境变量包含所有依赖,使用命令检查 JAR 包内容

        jar -tvf mylib.jar | grep "ClassName.class"

        解决方法:添加

        缺失依赖或修正类名

        2、NoClassDefFoundError

        现象:编译时存在类,但运行时找不到类定义

        原因:

        • 类初始化失败(如静态代码块抛出异常)。

        • 类文件被修改或损坏。

        • 类加载后又被卸载(如热部署场景)。

        问题定位:堆转储分析、检查类初始化逻辑

        解决方法:修复静态代码块异常或类文件损坏

        3、依赖冲突:NoSuchMethodError / NoSuchFieldError

        现象:调用方法或字段时找不到。

        原因:

        • 依赖冲突:多个 JAR 包包含同名但版本不同的类。

        • 编译环境和运行环境的类版本不一致。

        问题定位:Maven/Gradle 依赖树分析

        Maven:

        mvn dependency:tree  # 生成依赖树,检查重复或冲突版本

        Gradle:

        gradle dependencies  # 查看依赖关系

        解决方法:排除冲突版本或强制指定版本

        Maven 排除冲突依赖

        <dependency>
            <groupId>com.example</groupId>
            <artifactId>lib-a</artifactId>
            <version>1.0</version>
            <exclusions>
                <exclusion>
                    <groupId>com.conflict</groupId>
                    <artifactId>lib-b</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        Gradle 强制指定版本

        configurations.all {
            resolutionStrategy.force 'com.example:lib-b:2.0'
        }

        4、元空间溢出

        现象:java.lang.OutOfMemoryError: Metaspace

        原因:

        • 动态生成大量类(如频繁使用反射、CGLIB 代理)。

        • 未设置元空间上限(默认无限扩展)。

        问题定位:jstat、堆转储分析

          使用 jstat 监控元空间使用情况:

        jstat -gcutil <PID> 1000  # 关注 MU(Metaspace Utilization)

          生成堆转储(jmap -dump),使用 Eclipse MAT 分析加载的类数量及来源

        解决方法:限制元空间大小,减少动态类生成

        -XX:MaxMetaspaceSize=256m  # 设置元空间上限

        5、类重复加载

        现象:同一类被不同类加载器多次加载,导致 instanceof 判断失效

        原因:自定义类加载器未正确隔离类(如 OSGi、Tomcat 的 WebApp 类加载器)

        问题定位:jcmd VM.classloaders 分析

                使用 jcmd 或 jstack 查看线程上下文类加载器

        jcmd <PID> VM.classloaders  # 打印类加载器层次(JDK 8+)

        解决方法:遵循双亲委派,隔离类加载器(如 Tomcat 为每个 WebApp 使用独立的 WebAppClassLoader

        四、线程和锁问题

        1、死锁(Deadlock)

        现象:线程互相等待锁(阻塞:BLOCKED),应用无响应,CPU使用率低

        原因:线程持有一个锁,同时请求其他线程的锁,并且不放弃已持有的锁,形成环形等待链

        问题定位:jstack获取线程转储,分析锁持有情况。

        1> 生成线程转储

        jstack <PID> > thread_dump.txt  # 生成线程转储文件
        或
        jcmd <PID> Thread.print  # 效果同 jstack

        2> 分析线程转储

        • 查找 BLOCKED 状态的线程
          在转储文件中搜索 java.lang.Thread.State: BLOCKED

        • 检查锁持有关系
          查看线程的堆栈跟踪,确定其持有的锁和等待的锁。

        • 识别循环等待链
          多个线程互相等待对方持有的锁,形成环路。

        示例输出:

        "Thread-1" #12 prio=5 os_prio=0 tid=0x00007f48740d2000 nid=0x5d4 waiting for monitor entry [0x00007f486b7f6000]
           java.lang.Thread.State: BLOCKED (on object monitor at 0x000000076abb88b0)
               at com.example.DeadlockDemo$2.run(DeadlockDemo.java:30)
               - waiting to lock <0x000000076abb88c0> (a java.lang.Object)
               - locked <0x000000076abb88b0> (a java.lang.Object)
        
        "Thread-0" #11 prio=5 os_prio=0 tid=0x00007f48740d0000 nid=0x5d3 waiting for monitor entry [0x00007f486b8f7000]
           java.lang.Thread.State: BLOCKED (on object monitor at 0x000000076abb88c0)
               at com.example.DeadlockDemo$1.run(DeadlockDemo.java:20)
               - waiting to lock <0x000000076abb88b0> (a java.lang.Object)
               - locked <0x000000076abb88c0> (a java.lang.Object)

        3> 工具辅助分析

        Arthas(阿里开源工具):实时监控线程和锁状态。

        thread -b  # 自动检测死锁并显示阻塞线程

        解决方法:

        1> 代码层面

        避免嵌套锁;按固定顺序获取锁;减少锁粒度:使用细粒度锁(如 ConcurrentHashMap 分段锁)代替全局锁通过 ReentrantLock.tryLock(timeout) 避免无限等待

        Lock lockA = new ReentrantLock();
        Lock lockB = new ReentrantLock();
        
        if (lockA.tryLock(1, TimeUnit.SECONDS)) {
            try {
                if (lockB.tryLock(1, TimeUnit.SECONDS)) {
                    try { ... } finally { lockB.unlock(); }
                }
            } finally { lockA.unlock(); }
        }

        2> 资源管理

        • 限制资源池大小
          避免线程池或连接池资源耗尽导致死锁。

        • 使用无锁数据结构
          如 AtomicIntegerDisruptor 环形队列。

        2、线程数过多

        现象:应用响应变慢或卡死;内存占用过高,大量线程可能触发OutOfMemoryError: unable to create new native thread;频繁Full GC;线程创建失败,抛出java.lang.OutOfMemoryError或java.lang.Error: unable to create new native thread

        原因:

        1> 线程泄漏(Thread Leak):线程未正确关闭(如未调用ThreadPoolExecutor.shutdown())。

        2> 线程池配置不合理:

        • 核心线程数(corePoolSize)或最大线程数(maxPoolSize)设置过高。

        • 任务队列(workQueue)无界,导致任务堆积后线程数激增。

        3> 业务代码问题:

        • 循环/递归中无节制地创建线程。

        • 同步代码块设计不合理(如死锁、长时间阻塞)。

        4> 第三方库或框架缺陷:某些库(如Netty、gRPC)可能因配置不当或Bug导致线程数失控。

        5> 操作系统限制:超过用户进程最大线程数限制(可通过ulimit -u查看)。

        问题定位:

        1> 查看线程数统计

        # 查看JVM进程ID
        jps -l
        # 统计线程数
        ps -T <pid> | wc -l

        2> 分析线程堆栈

        # 生成线程快照
        jstack <pid> > thread_dump.txt
        # 或使用Arthas的`thread`命令实时分析
        thread -n 10  # 查看最活跃的10个线程

        3> 跟踪线程数变化

        JConsole:连接目标 JVM 后,进入 线程 标签页,实时查看活动线程数及状态分布。

        4> 分析线程状态

        • BLOCKED/WATING:可能因锁竞争或I/O阻塞。

        • RUNNABLE:高CPU线程可能是业务热点。

        5> 代码审查

        • 检查线程池使用是否规范(如是否调用shutdown())。

        • 搜索代码中new Thread()ExecutorService的创建点。

        解决方法:优化线程池配置,减少线程创建(如使用异步非阻塞模型)。

        1> 修复线程泄漏

        • 确保线程池正确关闭(使用shutdown()shutdownNow())。

        • 使用try-finallytry-with-resources管理线程资源。

        案例:线程池未关闭导致线程数持续增长

        • 定位:Arthas的thread --state WAITING显示大量线程处于等待任务状态。

        • 解决:添加Runtime.getRuntime().addShutdownHook()确保线程池关闭。

        2> 优化线程池配置

        • 设置合理的corePoolSizemaxPoolSize(根据CPU核数和任务类型)。

        • 使用有界队列(如ArrayBlockingQueue)避免任务堆积。

        • 配置拒绝策略(如ThreadPoolExecutor.CallerRunsPolicy)。

        案例:日志中频繁出现OutOfMemoryError: unable to create new native thread

        • 定位:通过jstack发现大量线程卡在第三方HTTP客户端的连接池等待。

        • 解决:调低连接池最大线程数,或改用连接复用(如HTTP/2)。

        3> 减少线程竞争

        • 优化锁粒度(使用分段锁或ReadWriteLock)。

        • 替换为无锁数据结构(如ConcurrentHashMap)。

        4> 调整JVM参数

        • 减少线程栈大小(-Xss256k,需权衡栈溢出风险)。

        • 调整系统级限制(如ulimit -u 65535)。

        5> 异步化改造

        • 使用响应式框架(如Reactor、RxJava)替代阻塞式多线程。

        • 采用协程(如Kotlin协程或Project Loom的虚拟线程)。

        6> 第三方库调优

        • 检查Netty的EventLoopGroup线程数配置。

        • 调整Tomcat的maxThreads(针对Web应用)。

        3、CPU飙升(热点代码)

        常见原因

        1> 无限循环或高CPU消耗的代码逻辑

                解决:增加合理的休眠(Thread.sleep())或退出条件,避免死循环或无阻塞的密集计算

        2> 线程竞争或锁争用:大量线程处于BLOCKED状态(如synchronized锁竞争),或自旋锁(CAS操作)未成功

                示例:ConcurrentHashMap的高并发扩容竞争、ReentrantLock未合理释放。

                解决:优化并发策略(如减少竞争粒度、改用LongAdder),减少锁竞争(如用并发容器、读写锁、分段锁)

        3> 外部资源调用阻塞:大量线程因IO或网络请求阻塞,但未正确释放CPU(如非阻塞模式未生效)

                示例:数据库查询未设置超时,线程长期阻塞等待响应。

        4> 频繁的垃圾回收(GC)

                解决:合理设置堆大小(避免GC频繁触发),选择低延迟GC算法(如G1、ZGC)

        5> 大量线程创建与销毁:线程频繁启动/停止(如不合理的线程池配置),线程调度开销大

                排查:检查线程池配置(核心线程数、队列容量等)

        6> JIT编译或代码优化问题:JIT编译器(C1/C2线程)在热点代码编译期间占用CPU。

                示例:高频方法被反复编译/去优化(如OnStackReplacement)。

                解决:调整JIT编译参数(如-XX:CompileThreshold

        排查步骤

        1> 定位高CPU进程/线程

        Step 1:使用top命令找到占用CPU高的Java进程(PID)。

        Step 2:通过top -Hp <PID>查看该进程内各线程的CPU占用,记录高CPU线程的ID(转为16进制,如printf "%x\n" 12345)。

        2> 分析线程堆栈

        Step 3:使用jstack <PID> > jstack.log导出线程堆栈。

        Step 4:在堆栈日志中搜索高CPU线程的16进制ID(如nid=0x3039),查看其执行代码逻辑。
                关键状态:
                        RUNNABLE:正在执行CPU密集型操作(如循环计算)。
                        BLOCKED:等待锁(检查锁竞争)。
                        WAITING/TIMED_WAITING:通常不占CPU,但需结合代码逻辑判断。

        3> 结合其他工具验证
        GC问题:使用jstat -gcutil <PID> 1000观察GC频率和内存区域变化。
        锁竞争:使用jstack或arthas的thread -b命令检测死锁。
        代码热点:使用async-profiler或Arthas的profiler生成火焰图,定位CPU热点方法。

        五、其他问题

        1、未启用压缩指针

        未启用压缩指针(-XX:+UseCompressedOops)导致64位环境内存浪费。

        在 64 位 JVM 中,未压缩的指针(对象引用)占 8 字节,而开启压缩后指针仅占 4 字节。启用 -XX:+UseCompressedOops 时,默认同时开启 -XX:+UseCompressedClassPointers,压缩类元数据指针(类型指针),进一步减少对象头大小(64 位下对象头从 16 字节压缩至 12 字节)

        优点:减少内存占用;提升缓存效率;支持更大堆内存;降低GC开销

        压缩范围限制:当堆内存超过 32GB 时,压缩指针无法覆盖全部地址空间,JVM 会自动关闭压缩功能。

        适用场景:

        • 64 位 JVM 且堆内存 ≤32GB(默认开启)。
        • 内存敏感型应用(如大数据处理、高并发服务)。

        验证:

        通过 java -XX:+PrintFlagsFinal -version 可验证参数是否启用

        2、JIT编译问题

        JVM 在运行时会将频繁执行的热点代码(HotSpot Code)通过 JIT 编译器转换为本地机器码,这些编译后的代码会存储在 ​​Code Cache​​ 中。ReservedCodeCacheSize 直接控制该缓存区的最大容量,确保编译后的代码能够被高效存储和复用,避免重复编译带来的性能损耗。

        表现:方法编译耗时高或代码缓存不足(CodeCache满)。

        调优:调整-XX:ReservedCodeCacheSize(为即时编译器(JIT)生成的本地机器代码预留内存空间),避免频繁去优化。可结合 -XX:+UseCodeCacheFlushing 参数启用缓存刷新机制,缓解空间不足问题

        注意:缓存区过小,可能导致频繁垃圾回收(即已编译的代码被清除)。当Code Cache被填满时,JVM 会停止进一步的编译优化,甚至禁用 JIT 功能,导致应用性能急剧下降

        参数调整依据:

        • 监控工具(如 jconsolejstat)观察 Code Cache 使用率,若频繁达到阈值(如 90% 以上)需增大该值。
        • 建议初始设置为 ​​256MB​​,并根据实际负载动态调整,一般不超过堆内存(-Xmx)的 ​​10%-20%​


        网站公告

        今日签到

        点亮在社区的每一天
        去签到