1. 问题引入:当你的Java服务突然“高烧不退”
做后端开发的朋友,尤其是负责线上服务稳定性的同学,肯定对下面这个场景不陌生:监控大盘突然告警,某个核心服务的CPU使用率飙到了90%以上,或者内存占用像坐了火箭一样直线上升,眼看就要触发OOM(OutOfMemoryError)。这时候,整个团队的气氛都会瞬间紧张起来,业务方在催,老板在问,而你盯着满屏的日志和曲线图,感觉无从下手。
这种“高烧”状态,我们称之为性能瓶颈或资源泄漏。它不像代码报错那样有明确的异常栈,更像是一个隐形的杀手,悄无声息地拖慢整个系统,最终导致服务不可用。定位这类问题,考验的不仅仅是编码能力,更是一套系统的“诊疗”思路和工具使用技巧。很多人一上来就胡乱重启,或者盲目加机器,这只能暂时掩盖症状,病根没找到,迟早还会复发。
今天,我就结合自己多年在电商、金融等高压场景下处理这类问题的经验,整理出一套从现象到根因的完整定位流程。这套方法不是死记硬背的命令集合,而是一个有逻辑、分层次的“破案”指南。无论你是刚接触线上问题的初级工程师,还是想梳理自己排查思路的资深开发者,相信都能从中获得直接的帮助。我们会从最外部的监控指标入手,像剥洋葱一样,层层深入到JVM内部、线程堆栈乃至具体的代码行,最终找到那个“罪魁祸首”。
2. 整体排查思路:建立系统化的诊断路径
面对CPU或内存过高的问题,最忌讳的就是毫无章法地东一榔头西一棒子。一个高效的排查过程,应该像医生问诊一样,有望、闻、问、切的步骤。我总结了一个四层递进的排查模型,可以帮助你快速建立诊断路径。
2.1 第一层:现象确认与初步归因
告警响了,第一步不是马上登录服务器,而是先花几分钟看清“战场”全貌。你需要确认几个关键问题:
- 是全局性问题还是局部问题?查看监控,是所有实例的CPU/内存都高了,还是仅仅其中某一台或某几台?如果是局部的,很可能与那台机器上的特定请求或数据有关;如果是全局的,则要考虑最近是否有统一的变更,比如发布新代码、更新配置、或流量突增。
- 是瞬时尖峰还是持续高位?观察监控曲线。如果是瞬间飙升然后回落,可能是正常的流量脉冲或某个耗时任务;如果是持续缓慢上升,直到打满,那极有可能是内存泄漏;如果是持续高位震荡,则可能是代码中存在低效循环或资源竞争。
- 关联指标有何异常?CPU高时,观察接口的QPS(每秒查询率)和RT(响应时间)。如果QPS没变而RT增加,可能是单个请求处理变慢;如果QPS和CPU同步飙升,可能是流量过大。内存高时,观察GC(垃圾回收)频率和耗时。如果Full GC频繁但回收效果甚微,基本可以断定是内存泄漏。
这个阶段,利用好公司的监控系统(如Prometheus + Grafana, Zabbix等)是关键。初步判断能帮你决定排查的紧急程度和大致方向。
2.2 第二层:定位问题进程与线程
确认了现象,下一步就是登录到目标服务器,找到具体的“病灶”。这里我们主要依赖操作系统层面的工具。
对于CPU过高:首先用top命令查看整体资源情况。按1可以显示每个CPU核心的利用率,看是否是某个核心被打满。记住异常Java进程的PID(进程ID)。 然后,使用top -Hp [PID]命令,查看该Java进程内所有线程的CPU占用情况。你会看到一串线程ID(TID)和对应的CPU百分比。那个占用最高的线程,就是我们要重点关注的“热点线程”。记下它的TID(十进制数字)。
对于内存过高:同样先用top命令,但关注RES(常驻内存集)和%MEM(内存使用百分比)列。找到内存消耗最大的Java进程PID。 注意,Linux的top看到的内存是进程占用的物理内存,而JVM的内存管理更为复杂,我们还需要结合JVM工具看内部各区域的使用情况,这会在下一层进行。
注意:
top中的内存信息有时会因共享内存等原因导致统计偏差。对于Java进程,更准确的做法是使用jstat或jcmd来观察JVM堆内存。
2.3 第三层:深入JVM内部洞察
这是定位Java应用问题的核心层。我们需要使用JDK自带的神兵利器。
工具一:jstack - 抓取线程快照jstack [PID] > thread_dump.log这个命令可以将指定时刻Java进程内所有线程的运行状态、调用栈信息输出到文件。拿到第二层找到的高CPU线程的TID(比如12345),我们需要将其转换为十六进制(0x3039),然后在thread_dump.log文件中搜索这个十六进制值,就能定位到该线程正在执行什么代码。如果它长时间停留在某个方法(比如一个死循环,或者一个锁等待),调用栈会清晰地告诉你。
工具二:jmap & jstat - 洞察内存奥秘
- jmap -heap [PID]:查看堆内存的总体配置和使用情况,包括各代(Eden, Survivor, Old)的容量、使用量、GC算法等。快速判断是否是堆空间设置不合理。
- jmap -histo:live [PID] | head -20:查看堆内存中存活对象的数量和大小统计,按总大小排序。排在前列的类,很可能就是内存消耗的“大户”或泄漏的嫌疑犯。注意:
-histo:live会触发一次Full GC,线上慎用!非紧急情况可以用-histo(不触发GC)先看个大概。 - jstat -gcutil [PID] 1000 10:每1秒(1000毫秒)输出一次GC统计信息,共10次。动态观察各内存区域的使用率(E, S0, S1, O, M分别代表Eden, Survivor0, Survivor1, Old, Metaspace)、GC次数(YGC, FGC)和耗时(YGCT, FGCT)。如果看到Old区(O)使用率不断上升,而FGC(Full GC)频繁但每次回收后O区使用率下降很少,这就是典型的内存泄漏迹象。
工具三:jcmd - 多功能瑞士军刀jcmd是JDK 7之后推荐的综合工具,功能强大且对进程影响相对较小。
jcmd [PID] Thread.print:等同于jstack,获取线程转储。jcmd [PID] GC.heap_info:查看堆信息。jcmd [PID] VM.native_memory:查看JVM申请的本地内存(堆外内存)详情,对于排查堆外内存泄漏(如Netty的Direct Buffer)至关重要。
2.4 第四层:代码级根因分析与验证
通过前三层,我们通常能锁定到可疑的线程栈和对象类。最后一层就是结合代码和业务逻辑进行验证。
- 分析线程栈:查看
jstack输出的热点线程栈。如果栈顶是java.lang.Thread.run,可能是个死循环的线程;如果栈顶是sun.misc.Unsafe.park或java.util.concurrent.locks.LockSupport.park,说明线程在等待锁,可能是锁竞争激烈导致CPU空转(虽然等待不消耗CPU,但争抢锁的过程会);如果栈顶是应用代码,比如com.xxx.service.Impl.process(),并且这个方法在栈里反复出现,那很可能就是这个方法逻辑有性能问题。 - 分析内存直方图:查看
jmap -histo输出中排名靠前的类。如果是业务相关的类(如OrderDTO,UserSession),且数量多得离谱,就要去代码里找这些对象是在哪里创建、为什么没有被回收。结合jstack,看是否有线程持有这些对象的巨大集合(如HashMap、ArrayList)的引用,导致GC无法回收。 - 代码审查与验证:根据线索去翻代码。常见的内存泄漏场景包括:静态集合类持续添加元素、未关闭的资源(如数据库连接、文件流、HTTP连接)、监听器或回调注册后未取消、使用ThreadLocal未及时清理等。常见的CPU热点场景包括:低效的算法(如多层嵌套循环)、正则表达式误用、频繁的日志输出(尤其是DEBUG级别)、锁粒度不合理等。
- 复现与修复:如果可能,在预发或测试环境尝试复现问题。可以尝试使用Arthas、Async-Profiler等更强大的在线诊断工具进行动态跟踪和采样分析,精准定位热点方法。修复后,必须通过压测验证效果。
这套四层排查路径,从宏观到微观,从系统到代码,形成了一个闭环。在实际操作中,你可能需要在不同层次间来回跳跃验证,但有了这个框架,你的排查就不会迷失方向。
3. CPU使用率过高问题深度定位
CPU使用率居高不下,意味着处理器一直在忙碌。对于Java应用,这通常不是“计算过于复杂”,而是“在不该忙碌的地方瞎忙”。下面我们拆解几种典型场景和对应的排查手术刀。
3.1 场景一:无限循环或低效算法
这是最直接的原因。某个线程陷入了一个无法退出的循环,或者一个算法的时间复杂度太高。
排查手法:
- 使用
top -Hp找到耗CPU的线程TID。 jstack获取线程转储,并转换TID为十六进制进行搜索。- 分析线程栈。如果你在栈顶看到类似下面的调用,并且栈深度很浅,反复循环,那很可能就是问题所在:
"Thread-0" #1 prio=5 os_prio=0 tid=0x00007f4874000 nid=0x3039 runnable [0x00007f48fe000] java.lang.Thread.State: RUNNABLE at com.example.BadService.infiniteLoop(BadService.java:20) // 卡在某个方法 at com.example.BadService.lambda$start$0(BadService.java:15) at com.example.BadService$$Lambda$1/0x0000000840060840.run(Unknown Source) at java.lang.Thread.run(Thread.java:748) - 使用Profiler工具进行热点方法采样。
jstack是瞬态图,如果问题不是持续占满CPU,可能抓不到。这时可以用Arthas的profiler命令,或者Async-Profiler,对CPU进行一段时间(比如30秒)的采样,它会生成一个火焰图。火焰图横向显示调用栈,纵向显示深度,最宽的那个“火苗”就是最耗CPU的方法。一眼就能看出系统的“热力”分布。
实操心得:
- 不要忽视“日志轰炸”。在循环里打
log.debug(...),如果日志框架配置不当(例如异步队列满),或者日志级别实际是INFO/ERROR但日志内容字符串拼接耗时,也可能导致CPU升高。检查日志配置和日志语句。 - 正则表达式,尤其是复杂的、在循环中编译的
Pattern.compile(),是CPU杀手。确保Pattern对象被缓存复用。
3.2 场景二:激烈的锁竞争
这是高并发场景下的典型问题。线程没有在执行业务计算,而是在为了获取锁而“自旋”空转,或者大量线程处于BLOCKED状态。
排查手法:
- 分析
jstack文件中的锁信息。搜索BLOCKED状态的线程。查看它们等待的锁是什么(waiting to lock <0x000000076bf62200>),以及是哪个线程持有了这个锁(locked <0x000000076bf62200>)。"Thread-2" #3 prio=5 os_prio=0 tid=0x00007f4874000 nid=0x303b waiting for monitor entry [0x00007f48fd000] java.lang.Thread.State: BLOCKED (on object monitor) at com.example.ResourceService.consume(ResourceService.java:50) - waiting to lock <0x000000076bf62200> (a java.lang.Object) // 等待这个锁 at com.example.Worker.run(Worker.java:25) "Thread-1" #2 prio=5 os_prio=0 tid=0x00007f4874000 nid=0x303a runnable [0x00007f48fe1000] java.lang.Thread.State: RUNNABLE at com.example.ResourceService.consume(ResourceService.java:55) - locked <0x000000076bf62200> (a java.lang.Object) // 正持有这个锁 at com.example.Worker.run(Worker.java:25) - 统计锁竞争热度。如果大量线程在等待同一个锁,说明这个锁的粒度太粗,成了性能瓶颈。可以使用Arthas的
monitor命令监控某个方法的调用耗时和成功率,或者使用thread -b命令直接找出当前阻塞其他线程最多的那个“罪魁祸首”线程。
解决方案:
- 缩小锁粒度:将一个大锁拆分成多个小锁,例如不用
synchronized锁整个方法,而是锁一个更小的对象。 - 使用并发容器:用
ConcurrentHashMap代替Collections.synchronizedMap(new HashMap())。 - 使用读写锁:对于读多写少的场景,使用
ReentrantReadWriteLock。 - 尝试无锁编程:考虑使用
Atomic变量或LongAdder。 - 检查死锁:
jstack输出最后一部分通常会做死锁检测,如果发现会明确提示Found one Java-level deadlock:。
3.3 场景三:频繁的GC活动
这是一个容易被忽略的间接原因。当应用程序产生大量短命对象,或者存在内存压力时,垃圾回收器(尤其是Young GC)会频繁启动。虽然单次GC停顿时间不长,但极高的频率会累积消耗大量CPU时间。
排查手法:
- 使用
jstat -gcutil [PID] 1000动态观察。重点看YGC(Young GC次数)和YGCT(Young GC总时间)这两列。如果YGC在短时间内(比如1分钟)疯狂上涨几十、上百次,并且YGCT累积时间很长,说明GC是CPU消耗大户。 - 结合
top观察。此时top看到的CPU高,可能不是业务线程,而是JVM的GC线程(如G1 Main Marker,GC Thread等)。
解决方案:
- 优化对象创建:减少不必要的对象创建,特别是在循环和高频调用路径上。重用对象(使用对象池需谨慎,避免引入复杂性)。
- 调整堆和代大小:如果Survivor区太小,会导致对象过早进入老年代,可能引发更耗时的Mixed GC或Full GC。适当调大年轻代(
-Xmn)可能有助于减少晋升。 - 选择合适的GC器:对于高吞吐量应用,Parallel GC可能更合适;对于低延迟要求,G1或ZGC是更好的选择。但这需要根据应用特性和性能测试来决定。
4. 内存使用率过高问题深度定位
内存问题比CPU问题更具潜伏性,往往在积累到一定程度后才突然爆发。其核心矛盾是:创建的对象速度 > 垃圾回收的速度。
4.1 场景一:堆内内存泄漏
这是最常见的Java内存问题。对象因为被错误的引用持有而无法被GC回收,随着时间推移,这些“垃圾”对象占满整个堆,最终触发OOM。
排查手法(标准流程):
- 监控观察:通过
jstat -gcutil观察老年代(O)使用率是否随时间持续上升,且Full GC(FGC)后回收效果很差(使用率下降不明显)。 - 生成堆转储文件:这是内存分析最关键的证据。在内存占用较高时,使用命令生成堆转储(Heap Dump)文件。
jmap -dump:live,format=b,file=heap.hprof [PID]:触发一次Full GC后转储存活对象,文件较小,但会干扰线上服务。- 推荐(JDK自带):
jcmd [PID] GC.heap_dump /path/to/heap.hprof:影响相对较小。 - 推荐(安全):在JVM启动参数中添加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump,让JVM在发生OOM时自动生成转储,最安全且能捕获到“案发现场”。
- 使用MAT或JVisualVM分析堆转储:
- MAT (Eclipse Memory Analyzer):功能极其强大。打开
heap.hprof文件后:- 首先看Leak Suspects Report(泄漏嫌疑报告),它会自动分析可能的内存泄漏点。
- 使用Histogram(直方图)视图,按类或类加载器统计对象数量和占用内存。与
jmap -histo类似,但更直观。 - 最关键的一步:找到疑似泄漏的大对象(比如一个巨大的
HashMap),右键选择Path To GC Roots -> exclude weak/soft references。这个功能会显示从GC根节点(如静态变量、活动线程栈)到这个对象的完整引用链。这条链就是阻止该对象被回收的“罪证”!通常你会发现,某个静态的Map或List在持续添加业务对象,却从未清理。
- JVisualVM:JDK自带,比较轻量。加载堆转储后,查看“类”视图,同样可以按大小排序,并查看实例的引用关系。
- MAT (Eclipse Memory Analyzer):功能极其强大。打开
常见泄漏点:
- 静态集合类:如
public static Map CACHE = new HashMap();是最经典的泄漏源。 - 连接池、线程池未正确配置:连接或线程创建后不释放。
- 监听器和回调:注册后没有注销。
- ThreadLocal使用不当:在线程池场景下,线程复用导致前一个任务的
ThreadLocal变量未被清除,从而随着线程存活一直累积。 - 内部类持有外部类引用:非静态内部类会隐式持有外部类实例的引用,如果这个内部类对象被长生命周期对象引用,会导致外部类实例也无法释放。
4.2 场景二:堆外内存泄漏
JVM的内存不止堆(Heap),还有堆外内存(Native Memory),也叫直接内存。像NIO的DirectByteBuffer、JNI调用、某些网络框架(如Netty)的池化内存管理,都会使用堆外内存。这部分内存不受JVM垃圾回收管理,需要手动释放或依赖框架的生命周期管理。
排查手法:
- 现象识别:
top看到进程的RES内存持续增长,但通过jstat或jmap看到的堆内存使用率却很正常,甚至很低。这就是堆外内存泄漏的典型特征。 - 使用
jcmd查看详情:jcmd [PID] VM.native_memory命令可以详细展示JVM内部各部分内存的使用情况,包括Internal(元数据)、Arena(分配器)、Thread(线程栈)、Code(JIT代码缓存)以及Direct(直接缓冲区)。关注Direct部分的committed内存是否异常高且持续增长。 - 使用操作系统工具:
pmap -x [PID] | sort -k3 -n -r可以查看进程的内存映射,找到那些巨大的匿名映射块(anon),它们可能对应着堆外内存区域。 - 定位代码:堆外内存泄漏定位更难。如果使用了Netty,可以开启Netty的泄漏检测工具(
ResourceLeakDetector)。对于DirectByteBuffer,可以尝试在代码中跟踪其创建和释放,或者使用Java Flight Recorder (JFR)来监控DirectByteBuffer的分配事件。
解决方案:
- 确保
DirectByteBuffer在使用后调用cleaner.clean()(通常通过((DirectBuffer) buffer).cleaner().clean())或等待GC触发其Cleaner机制。但在高并发下,最好池化复用。 - 检查使用的第三方库(尤其是网络库、序列化库)是否有已知的堆外内存泄漏问题,并升级到修复版本。
4.3 场景三:元空间(Metaspace)溢出
Java 8之后,永久代(PermGen)被元空间(Metaspace)取代。它主要存储类的元数据(Klass结构、方法信息等)。如果应用动态生成大量类(例如大量使用CGLib、ASM进行字节码增强,或者Groovy等脚本引擎频繁编译),就可能导致元空间溢出,错误为Metaspace。
排查手法:
- 监控观察:
jstat -gcutil中的M(Metaspace)使用率是否接近100%。 - 分析原因:元空间溢出几乎总是和动态类加载有关。检查应用是否使用了大量的反射、动态代理、或者像Spring AOP(CGLib代理)在反复创建代理类。
解决方案:
- 适当调大元空间大小:
-XX:MaxMetaspaceSize=256m。 - 更根本的是,优化应用设计,减少不必要的动态类生成。例如,检查是否有循环里在反复创建代理对象。
5. 高级工具与实战技巧
除了JDK自带的基础工具,掌握一些更强大的“武器”能让你在排查时事半功倍。
5.1 Arthas:阿里开源的在线诊断利器
Arthas不需要修改代码、不需要重启服务,就能动态跟踪问题,是线上排查的“核武器”。
常用命令场景:
dashboard:实时仪表盘,一眼看清线程、内存、GC、运行时信息。thread [PID]或thread -n 3:查看所有线程状态,或CPU占用率最高的前3个线程。比top -Hp+jstack方便太多。thread -b:一键找出当前阻塞其他线程最多的线程(死锁或锁竞争瓶颈)。jad com.example.BadService infiniteLoop:反编译线上正在运行的类字节码,直接看源码。当你怀疑代码版本不对时特别有用。watch com.example.Service methodName '{params, returnObj, throwExp}':动态观察方法调用的入参、返回值和异常。用于追踪数据流向。profiler start/profiler stop:生成CPU或内存分配的火焰图,图形化展示热点。heapdump:在线生成堆转储文件,功能同jmap -dump。
实操心得:Arthas的命令非常丰富,刚开始不必全部掌握。记住dashboard、thread、jad、watch、profiler这几个最常用的,就能解决80%的问题。使用前最好在测试环境熟悉一下,避免对线上服务造成意外影响。
5.2 性能剖析与火焰图
火焰图是性能分析的“心电图”,它能将复杂的调用栈和耗时情况可视化。
- CPU火焰图:看最顶层的“平顶山”,那里就是消耗CPU最多的调用链。
- 内存分配火焰图:看哪些方法在频繁地分配对象。
生成方法:
- 使用Async-Profiler:下载工具,通过
profiler.sh脚本附加到Java进程,可以生成非常清晰的SVG格式火焰图。 - 使用Arthas的profiler命令:集成在Arthas中,使用更方便,
profiler start开始采样,profiler stop停止并生成火焰图文件。
看图技巧:
- X轴不代表时间,而是采样数量,宽度越宽,表示被采样到的次数越多,即越耗时。
- Y轴是调用栈深度,从上到下是调用关系。
- 鼠标悬浮可以看具体方法的采样百分比。
- 寻找最宽且位于中下层的“火苗”,那就是你要优化的热点方法。
5.3 GC日志分析与优化建议
GC日志是理解JVM内存行为的金钥匙。务必在启动参数中开启GC日志。
推荐参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -Xloggc:/path/to/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M对于JDK 9+,推荐使用更统一的日志框架:
-Xlog:gc*,gc+age=trace,safepoint:file=/path/to/gc.log:time,uptime,level,tags:filecount=5,filesize=10M分析要点:
- Young GC频率和耗时:如果频率过高(如几秒一次),考虑增大年轻代(
-Xmn)。如果单次耗时过长,检查是否有很多“大对象”直接进入了老年代(参数-XX:PretenureSizeThreshold)。 - Full GC频率和原因:关注每次Full GC的触发原因(如
Allocation Failure,Metadata GC Threshold,System.gc())。如果是由Allocation Failure触发且频繁发生,说明老年代空间不足,可能需要增大堆(-Xmx)或优化程序减少内存占用。 - GC后内存回收效果:看每次GC后,各区域的使用量下降是否明显。如果老年代每次Full GC后使用率下降很少,就是内存泄漏的铁证。
优化思路:
- 首要目标:避免或减少Full GC。
- 核心策略:让对象在年轻代就“死掉”。调整
-Xmn(年轻代大小)、-XX:SurvivorRatio(Eden和Survivor比例),让对象有足够的时间在Young GC中被回收。 - 选择GC器:吞吐量优先用Parallel GC,低延迟优先用G1或ZGC。G1需要设置合理的
-XX:MaxGCPauseMillis(期望最大停顿时间,如200ms),ZGC则几乎全程并发,停顿时间极短,但吞吐量可能略有损耗。
6. 常见问题排查清单与避坑指南
这里将高频问题整理成表,方便你快速对照排查。
| 现象 | 可能原因 | 排查工具/命令 | 关键证据/下一步动作 |
|---|---|---|---|
| CPU使用率持续100% | 1. 某个线程死循环 2. 频繁的Young GC 3. 锁竞争激烈(自旋) | 1.top -Hp+jstack2. jstat -gcutil3. jstack看BLOCKED线程 | 1. 找到热点线程栈,定位代码。 2. 观察YGC次数是否暴增。 3. 找到被大量线程等待的锁。 |
| CPU使用率间歇性飙升 | 1. 定时任务触发 2. 外部调用超时/重试风暴 3. 缓存失效导致的雪崩查询 | 1. 关联监控时间点与任务日志。 2. 检查依赖服务状态和超时配置。 3. 分析调用链,查看数据库/缓存慢查询。 | 1. 检查调度中心日志。 2. 使用Arthas trace命令追踪调用链耗时。3. 查看DB监控和慢SQL日志。 |
| 堆内存缓慢增长直至OOM | 内存泄漏(对象无法回收) | 1.jstat -gcutil观察O区趋势。2. jmap -histo或jcmd GC.heap_info3. 生成堆转储用MAT分析。 | 1. O区使用率只升不降,FGC无效。 2. 发现某个业务类实例数异常多。 3. MAT中找到该对象的GC Roots引用链。 |
| 物理内存高但堆内存使用正常 | 堆外内存泄漏(Direct Buffer等) | 1.top看RES,jstat看堆。2. jcmd VM.native_memory3. pmap -x查看进程内存映射。 | 1. RES高,堆内存使用率低。 2. Native Memory的 Committed持续增长。3. 存在巨大的匿名内存映射块。 |
| 服务卡顿,但CPU不高 | 1. 外部依赖(DB、Redis、RPC)慢 2. 锁竞争导致线程大量 WAITING3. 频繁的Full GC导致“世界暂停” | 1. 链路追踪(SkyWalking, Zipkin)。 2. jstack统计线程状态比例。3. 分析GC日志,看Full GC频率和耗时。 | 1. 发现某个下游调用耗时极长。 2. 大量线程处于 WAITING (parking)。3. GC日志中出现长停顿的Full GC记录。 |
| Metaspace溢出 | 动态类加载过多(CGLib代理等) | 1.jstat -gcutil看M区。2. 检查是否大量使用反射、动态代理。 | 1. M区使用率100%。 2. 错误信息为 Metaspace。增加-XX:MaxMetaspaceSize并优化代码。 |
避坑指南与心得:
- 预防大于治疗:在编码阶段就建立良好的习惯。避免在循环中创建大量临时对象、谨慎使用静态集合、及时关闭资源(用try-with-resources)、合理设计缓存失效策略。
- 监控与告警先行:务必搭建完善的应用性能监控(APM)体系。监控核心指标:CPU使用率、堆内存使用率、GC频率与耗时、接口QPS/RT、错误率。设置合理的告警阈值,能在问题萌芽阶段就发现。
- 保留现场:遇到线上问题,条件允许的话,先保留现场(如线程Dump、堆Dump),再重启。重启会丢失一切线索。可以设置JVM参数
-XX:+HeapDumpOnOutOfMemoryError自动留证。 - 理解工具原理:不要死记命令。理解
jstack是看线程瞬间的快照,jstat是看JVM内部的趋势,jmap是看内存的静态分布。根据不同场景选择合适的工具。 - 保持怀疑,交叉验证:工具给出的信息有时会有误导。比如
top看到的CPU高,不一定是Java业务代码,可能是GC。需要用jstat和线程栈交叉验证。MAT分析出的“泄漏点”,也要回到代码逻辑中确认是否合理。 - 性能优化要有数据支撑:不要凭感觉优化。优化前用工具(Arthas profiler, JMH基准测试)定位真正的瓶颈,优化后用同样的工具和数据对比验证效果。