十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JVM监控与故障排查工具实战:从原理到选型再到定位

JVM监控与故障排查工具实战:从原理到选型再到定位 做Java开发久了总会遇到那么一两次生产事故。应用突然CPU飙升到100%或者半夜收到内存告警再或者GC停顿让接口响应从50毫秒变成5秒。这时候如果手里没有一套JVM监控与故障排查工具面对的全是黑盒基本只能靠重启缓解症状然后祈祷别再发生。这篇文章我想把这些年用过的JVM监控和排查工具完整梳理一遍从基础原理讲到工具选型再到实际故障怎么一步步定位最后整理几个高频问题的排查套路。内容既照顾刚接触JVM的后端新人也适合做运维、SRE的同学直接拿去落地。我的经验是JVM监控这件事难点从来不是某个工具不会用而是你不知道该看什么指标、指标异常后又该查哪里。所以文章不会只堆命令我会把每一步背后的原理讲清楚这样换个工具你也能举一反三。1. JVM到底在监控什么先对齐基础模型1.1 运行时数据区堆、栈、元空间监控JVM之前先得搞清楚JVM把内存花在哪了。很多人一上来就盯-Xmx以为堆起来就完事结果元空间溢出、直接内存溢出照样把你干趴下。JVM运行时数据区大致分这么几块线程私有的虚拟机栈、本地方法栈、程序计数器线程共享的堆、方法区JDK8以后叫元空间Metaspace还有一块常被忽略的堆外直接内存。堆是对象分配的主战场绝大多数字节码创建的对象都放在这里堆里又分新生代和老年代新生代里再拆Eden区和两个Survivor区。默认情况下新生代和老年代的大小比例大约是1:2Eden区和Survivor区比例是8:1:1具体可以通过-XX:NewRatio和-XX:SurvivorRatio调整。元空间很多人不熟它是JDK8干掉永久代之后的产物存的是类元数据、方法信息、常量池这类东西。JVM规范里元空间属于本地内存不受-Xmx限制。这就是为什么有时候堆只有2G但进程RSS已经涨到3G多——元空间、JIT编译产物、线程栈、堆外缓冲都在悄悄吃内存。排查这类问题光看java.lang.OutOfMemoryError: Java heap space是不够的还得学会看Metaspace、Direct buffer memory这类错误类型。JRE和JVM的关系也顺带提一句JRE是Java运行环境包含JVM和核心类库JVM只是JRE里负责执行字节码的那部分。你监控的、调参的、出问题的其实都是这个JVM实例类库只是它的“外挂装备”。1.2 GC回收器与停顿模型监控JVM一半的指标都和GC有关。GC的全称是Garbage Collection它做的事情就是在内存不够或者到达触发条件时回收那些再也用不到的对象。GC需要从GC Roots出发遍历对象引用图能到达的对象保活到达不了的对象就是垃圾。这个过程里有个核心概念叫STWStop The World也就是垃圾回收器在工作时业务线程必须停下来不然一边清理一边分配对象引用关系就乱了。GC回收器发展到现在主流的有Serial、Parallel、CMS、G1还有JDK11以后开始成熟的ZGC和Shenandoah。JDK8默认的是Parallel Scavenge加Parallel Old追求的是吞吐量JDK11开始默认变成G1把堆分成一个个Region通过混合收集来控制停顿时间ZGC更进一步把STW时间压到毫秒级甚至更低适合超大堆和低延迟场景。我拿仓库打比方堆就是仓库GB对象是货GC就是保洁员。Parallel是那种“一次性大扫除”的效率型保洁仓库容量利用得高但清扫时全员放假G1是分区域打扫的保洁先扫垃圾最多的区尽量不让仓库停止营业太久ZGC则像“不停业清洁”业务基本无感但保洁设备本身比较复杂。监控时你真正关心的不是GC用哪个回收器而是GC的频率和时长。如果Minor GC每秒钟都来一次或者Full GC一次停顿好几秒业务用户能直接感受到卡顿。所以后面所有监控大盘的核心指标都是围绕GC次数、GC耗时、堆占用走势来展开的。1.3 监控指标到底看哪些工具千千万指标就那么几个。JVM监控的黄金指标我习惯分成四类堆内存已用堆、最大堆、Eden、Survivor、老年代各自的占用和增速。GC行为YGC次数、YGC耗时、Full GC次数、Full GC耗时、GC后堆占用回落情况。线程与类当前活跃线程数、阻塞线程数、死锁风险、类加载总数。进程级CPU占用、RSS内存、文件描述符数、系统负载。进程级的CPU和内存必须和JVM内部指标一起看。很多JVM层面的怪现象根因其实在容器上——比如K8s里JVM没识别到容器限制默认拿宿主机内存当参考直接给你分配一个超大的堆容器OOM Kill之后表现为进程突然消失。这个坑后面的实操部分我会再展开。2. 工具全景从JDK自带命令到可视化全家桶2.1 JDK自带命令行工具jps / jstat / jmap / jstack / jcmd哪怕你周围有再多的监控平台我也建议先把JDK自带的命令行工具练熟。为什么因为生产环境断网、安全限制、平台没接入都是常态关键时刻能救命的往往就是这几个命令。jps是最常用的进程查看工具类似Linux的ps但它只看Java进程。我一般这么用jps -lv-l输出完整主类名-v显示传给JVM的参数。这个命令能让你快速确认当前机器上有几个Java进程、各自什么启动参数、谁是PID。注意如果进程不是当前用户启动的可能看不到必要时加sudo。jstat是JVM统计信息工具最经典的用法是看GC情况jstat -gcutil pid 1000 10意思是每秒输出一次GC统计连续输出10次。输出里的S0、S1、E、O分别对应两个Survivor区、Eden区和老年代的使用百分比YGC是Minor GC次数YGCT是Minor GC总耗时FGC是Full GC次数FGCT是Full GC总耗时。这个命令最大的优点是开销非常低线上长期开着也不怕。jmap用来做堆快照和堆信息查看最常用的是导出heap dumpjmap -dump:live,formatb,file/tmp/app.hprof pid但这里必须提醒一句jmap -dump在导堆快照时会触发一次Full GC生产大堆环境下可能会造成明显的业务停顿。所以这个操作最好在低峰期做或者和团队确认过再执行。只看堆信息的话用jmap -heap pid会温柔一些。jstack导出线程快照故障排查里出现频率极高jstack pid thread.log拿到日志后grep关键字找java.lang.Thread.State重点看RUNNABLE结合业务代码栈以及WAITING、BLOCKED一堆的情况。这个命令在CPU飙高、线程死锁的场景下是主力工具后面实战部分会演示具体用法。jcmd是JDK7以后加入的“瑞士军刀”很多jmap、jstack的功能它都能做而且不用频繁attach。比如查看帮助jcmd pid helpJFRJava Flight Recorder在JDK11之后也可以用它来开启jcmd pid JFR.start duration60s filename/tmp/app.jfr生产环境如果不想引入额外agentJFR的开销很低是很好的现场录制方案。2.2 可视化单体工具JConsole / VisualVM / JMC命令行适合快速定位但要做趋势分析、看堆里到底是什么对象占内存可视化工具更直观。JConsole是JDK自带的老牌工具通过JMX协议连接本地或远程JVM能看内存、线程、类加载、MBean信息还能手动触发GC。适合临时连上去瞄一眼但不适合深度分析。远程连接时需要在启动参数里加上JMX配置-Dcom.sun.management.jmxremote \ -Dcom.sun.management.jmxremote.port9010 \ -Dcom.sun.management.jmxremote.authenticatetrue \ -Dcom.sun.management.jmxremote.sslfalse注意生产环境一定要开认证并且建议只在内网使用否则相当于给攻击者开了一扇门。VisualVM是Oracle早期力推的工具能同时看本地进程和远程进程还能装插件做BTrace动态追踪。不过版本更新变慢新JDK下偶尔有兼容问题我更多拿它来分析已有的heap dump文件比如用它的“堆查看器”快速看大对象和类实例数。JMCJDK Mission Control和JFR是绝配尤其是JDK11以后JMC从独立下载变成了开源项目。JFR负责低开销录制JMC负责可视化分析能看到GC停顿时间线、锁竞争、IO、类加载、方法抽样等非常细的信息。遇到那种“偶发卡顿”的疑难问题我会开着JFR录半小时再让JMC分析GC和锁区间基本能定位出问题所在。2.3 分布式监控套装Prometheus Grafana、Zabbix、ELK/Kafka单体机器用上面的工具就够了但一个集群几十台节点、几十个Java服务没有集中监控平台不行。目前主流有两大流派。第一个是云原生时代的Prometheus Grafana。Prometheus是拉模式采集Java应用通过jmx_exporter或者Micrometer暴露HTTP指标端点Prometheus定期拉取Grafana负责展示Alertmanager负责告警。这套方案在K8s里几乎是标准配置Spring Boot应用只要引入micrometer-registry-prometheus再配一条scrape_configs就能接入。后面实操部分我会给出一套能直接用的配置。第二个是传统运维场景里常见的Zabbix。Zabbix有Java Gateway可以监控JMX指标而且它本身就是agent模式适合已经用Zabbix管了操作系统、网络设备、中间件的团队。新主机要纳入监控装个zabbix agent并把server地址指过去再在Web端添加主机和监控项即可。不过Zabbix的JVM监控粒度偏粗细粒度GC曲线、线程状态、堆结构不如Prometheus灵活。日志这块通常是ELKElasticsearch Logstash Kibana或者EFK再配合Kafka做削峰缓冲。JVM监控里日志的价值集中在GC日志、OOM异常堆栈、业务异常日志。有了集中日志系统故障时可以按时间线把GC日志和业务错误日志拼在一起很有用。但要注意日志系统是辅助定位不是实时指标告警的替代品。我的选型建议很简单公司已经有Zabbix体系、不想再引一套存储就先在Zabbix里扩展JVM监控如果是从零搭建或者已经在K8s上闭眼选PrometheusGrafana日志统一走ELKKafka按日志量决定要不要加。3. 实操从零搭建一套JVM可观测体系3.1 单机快速诊断流程一次模拟故障排查先演示一个最常见的现场故障定位流程JVM进程CPU飙高且内存疑似泄漏。假设进程PID是2345。第一步确认进程和负载top -Hp 2345-H按线程维度展示CPU占用你会看到一个或多个线程的CPU接近100%。记下那个线程的PID比如是2350。第二步把线程PID转成十六进制因为jstack输出的线程nid是十六进制printf %x\n 2350输出大概是92e。然后导出线程栈jstack 2345 /tmp/jstack.log在/tmp/jstack.log里搜索nid0x92e就能看到这个线程正在执行什么代码。很多时候死循环、锁等待、IO阻塞的元凶一眼就看出来了。第三步用jstat确认内存和GC状态jstat -gcutil 2345 1000 5如果老年代O已经99%并且FGC持续增长基本可以判断有内存压力。这时再决定要不要导出堆快照jmap -dump:live,formatb,file/tmp/2345.hprof 2345导出的hprof文件用MAT或者VisualVM打开看Dominator Tree支配树里前几个大对象找到持有它们的最短GC Roots路径就能定位到是哪个业务对象被缓存/静态集合一直持有着。这是一套“现场三板斧”top找线程 - jstack定位代码 - jstat/jmap判断内存。建议每个Java开发都练到肌肉记忆。3.2 用PrometheusGrafana搭建监控大盘下面给一套能直接落地的Prometheus方案。第一步Java应用暴露指标。Spring Boot应用在pom.xml里加依赖dependency groupIdio.micrometer/groupId artifactIdmicrometer-registry-prometheus/artifactId version1.12.5/version /dependency然后启动参数里加-Dmanagement.endpoints.web.exposure.includehealth,prometheus启动后访问/actuator/prometheus能看到标准指标。如果是非Spring Boot的普通Java应用就用jmx_exporter下载jar后先给一份配置文件jmx_exporter_config.yml里面定义抓取哪些MBeanrules: - pattern: java.lang:typeMemory name: jvm_memory_used_bytes labels: area: heap attrNameSnakeCase: true启动时用-javaagent:/path/jmx_prometheus_javaagent.jar8081:jmx_exporter_config.yml暴露端口。第二步配置Prometheus抓取任务。编辑prometheus.ymlscrape_configs: - job_name: java-app scrape_interval: 15s static_configs: - targets: [192.168.1.10:8081]scrape_interval我建议设15秒太频繁除了徒增压力外没意义太慢又容易漏掉瞬时高峰。如果服务数量很多要按每秒抓取的目标数估算一下Prometheus本机负载。第三步Grafana导入面板。社区有现成的JVM (Micrometer)面板可以直接搜dashboard id: 4701这类的现成面板导入后设置数据源为Prometheus就能看到堆使用率、GC次数、GC耗时、线程数、类加载数等核心图表。这套方案最大的好处是扩展容易新服务只要暴露端口加一行targets就完事。3.3 告警规则设置监控不等于告警没告警的监控会变成“出了事才知道大盘在报警”。我建议先配下面这几条规则。堆内存使用率超过90%持续5分钟- alert: JVMHeapUsageHigh expr: jvm_memory_used_bytes{areaheap} / jvm_memory_max_bytes{areaheap} 0.9 for: 5m labels: severity: warning annotations: summary: JVM heap usage high注意这里要排除-1这种取不到最大值的情况不然表达式会算出异常值。更稳妥的写法是先过滤掉jvm_memory_max_bytes 0。GC耗时告警比如Full GC次数在5分钟内增长超过阈值- alert: JVMFrequentFullGC expr: increase(jvm_gc_pause_seconds_count{actionend of major GC}[5m]) 3 for: 2m labels: severity: critical还有线程数告警比如活跃线程数超过基线太多。基线怎么定可以先观察一周取P95值再把告警阈值设在P95的1.5倍左右。没有基线的告警不是误报就是漏报。告警分级也很重要。我习惯把内存超阈值这种可能拖垮业务的设为criticalGC次数略多先给warning避免凌晨两点因为一条warning把所有人吵醒。3.4 开发环境JVM参数调整IDEA/Gradle daemonJVM监控不只是生产环境的事开发环境同样会踩坑。最典型的就是IDE或者构建工具本身OOM。以IDEA为例它本质也是个JVM程序默认堆大小可能不够项目多的时候用。如果你经常遇到IDEA卡顿、报OutOfMemoryError可以去Help - Change Memory Settings或者在安装目录下改idea64.exe.vmoptions。我常用的配置-Xms512m -Xmx2048m -XX:MaxMetaspaceSize1g不建议盲目把-Xmx拉到4G以上尤其是笔记本内存只有16G的时候IDEA吃得太多Docker、浏览器、数据库全都跟着遭殃。MaxMetaspaceSize设一个上限能防止项目类太多时元空间无脑膨胀。Gradle daemon是另一个高频坑。很多人报错expiring daemon because jvm heap space is exhausted就是Gradle后台守护进程堆太小。在gradle.properties里加org.gradle.jvmargs-Xmx2048m -XX:MaxMetaspaceSize512m改完重启Gradle daemon这个问题基本就解决了。遇到构建工具OOM优先去看它的启动JVM参数而不是怀疑代码写错了。4. 典型故障排查实战与坑位总结4.1 OOM先看dump还是先看日志OOM的表现不只有Java heap space一种。常见的还有Metaspace类加载过多常见于频繁热部署、动态代理类爆炸、反射大量生成类。Direct buffer memory堆外内存使用过度常见于Netty、RocketMQ这类大量使用堆外缓冲的框架。unable to create new native thread线程数超过系统限制常见于线程池无界、每次请求都new线程。GC overhead limit exceededGC基本不回收对象堆已经病入膏肓。遇到OOM我建议先看监控把这个时间段前后的堆使用、GC频率、线程增长画出来再决定要不要dump。很多人一上来就dump大文件结果分析半天发现是线程数打爆不是堆的问题。如果是堆OOMdump分析是最直接的手段。工具选MAT或者JProfiler都行重点看“Leak Suspects”和“Dominator Tree”。举个例子之前排查过一个接口偶尔OOM堆里全是一个ArrayList的实例引用顺着GC Roots往上找发现是静态缓存Map没做大小限制每个请求都往里塞数据。这种问题不看dump光看监控曲线只能猜。4.2 CPU飙高线程栈定位三板斧CPU问题我一般按这个顺序处理top -Hp pid printf %x\n thread-id jstack pid /tmp/stack.log grep -A 30 nid0xhex /tmp/stack.log看到栈里的业务方法后通常有三种结果死循环、正则回溯、频繁GC。死循环直接看代码正则回溯会看到java.util.regex相关的栈频繁GC则要配合jstat看是不是内存一直在分配、回收。还有一类情况是线程栈看起来没异常CPU还是高那就要怀疑是不是JIT编译占用的CPU。可以看jstat -compiler pid或者用JFR录制一段看HotSpotCompilerThread。这种情况通常无伤大雅属于新代码刚启动时的预热阶段。4.3 GC频繁且停顿长GC日志解读GC日志是排查GC问题的第一手资料。先确认你有没有在启动参数里开启GC日志。JDK8及以前用-Xloggc:/data/logs/gc.log -XX:PrintGCDetails -XX:PrintGCDateStampsJDK11及以后推荐用新版日志-Xlog:gc*:file/data/logs/gc.log:time,uptime,level,tags拿到日志后不要一行行肉眼看可以用GCeasy或者GCViewer解析直接看吞吐量、停顿时间分布、各代使用情况。重点关注两点一是YGC后Eden回收是否正常二是每次晋升到老年代的对象大小是否异常。典型问题有几个。第一个是分配速率太高比如日志显示每分钟YGC上百次说明代码在大量创建短生命周期对象这种要优化代码而不是调堆。第二个是大对象直接进入老年代可以通过-XX:PretenureSizeThreshold控制但更推荐先排查是哪些大对象。第三个是晋升阈值过低导致对象过早进入老年代可以让GC日志里带上-XX:PrintTenuringDistribution看看年龄分布再决定要不要调MaxTenuringThreshold。4.4 常见问题速查表现象可能原因第一步排查常用工具进程突然消失无OOM日志容器/系统OOM Killdmesg查内核日志检查容器内存限制top、dmesg堆内存持续增长不回落对象被全局引用、缓存没清理dump后看GC Roots路径jmap、MAT、VisualVM老年代疯狂增长FGC频繁晋升对象过多、大对象直入看GC日志晋升量、年龄分布jstat、GCeasyCPU 100%但GC正常死循环、正则回溯、锁竞争jstack定位线程栈jstack、JFR线程数飙升到几千线程池未限流、线程泄漏jstack统计线程栈出现次数jstack、JMC元空间OOM类加载过多、热部署泄漏查类加载数dump类加载器jcmd、MAT开发工具/构建工具OOMIDE或daemon堆太小调整vmoptions/gradle.properties手动调参这里特别提醒一个容器环境的坑新版JDK虽然默认开启UseContainerSupport会自动识别容器内存限制但如果你手动设了-Xmx它还是会以-Xmx为准。建议在容器里用比例参数-XX:MaxRAMPercentage75.0 -XX:InitialRAMPercentage50.0这样JVM就能跟随容器内存动态调整而不是怼着一堆写死的-Xmx跑。嵌入式或者资源极小的环境也是一样的道理。资源紧张时别开一堆agent优先用JFR录制短时片段配合jstat这种低开销命令做快照式监控。监控工具本身也是要吃饭的别让监控把服务搞挂了。最后再分享一个我最近踩过的坑某次做JVM监控大盘刚开始把Prometheus采集间隔设成5秒采集目标又多结果Prometheus所在机器CPU直接飙到80%。后来把间隔调到30秒告警规则改成分级整个体系才稳下来。监控方案不是越密越好合适的采集频率和清晰的告警规则比花哨的大屏重要得多。
返回列表