上一系列《JVM 深入》(共三篇)讲清了 JVM 内部「长什么样、怎么干活」。这一篇解决另一半问题 —— 线上运行时,怎么实时看见它的状态、并据此定位问题。
知道了 JVM 有堆、有 GC、有线程,可线上服务半夜内存飙高、Full GC 频发的时候,你总不能一台台机器去敲 jstat。
真正的工程做法是:让每个 JVM 持续把自己的状态「播报」出来,集中采集、画成图、设好告警。这套最主流的组合就是 Micrometer / JMX Exporter(暴露指标)+ Prometheus(采集存储)+ Grafana(可视化)。这篇我们就把这条链路从头到尾走一遍,重点落在 每张图到底在说什么、怎么从图里看出问题。
TL;DR
- 监控链路一句话:暴露(Micrometer / JMX Exporter)→ 采集(Prometheus 定时 Pull)→ 可视化(Grafana 看板)→ 告警(Alertmanager)。
- 暴露指标:Spring Boot 加
actuator + micrometer-registry-prometheus,放出/actuator/prometheus;非 Spring 挂 JMX Exporter agent。两条路线指标命名不同,导看板时要对上。 - 采集 + 看板:Prometheus 配
scrape_configs定时抓取,targetUP即通;Grafana 直接导入现成看板 ID(4701 最经典)。 - 读看板三条铁律:① 别被单个百分比吓到(Heap used 76% 拆到分代常是 Eden「虚高」,看老年代趋势才算数);② 业务指标和 JVM 指标要对照着看;③
No data多半是查询 / 标签问题,不是故障。 - 判断泄漏的金标准:老年代基线是否台阶式上涨;健康的堆是「能回到低位的锯齿」。
- 告警别成噪音:按 warning(趋势)/ critical(已影响)分级、给足
for滤毛刺、每条对应一个动作。
Table of contents
Open Table of contents
一、先看全景:数据怎么从 JVM 流到 Grafana
在动手之前,先建立一张「数据流动图」,后面所有配置都是在填充这张图的某一环:
监控链路全景:应用把指标挂成 HTTP 端点 → Prometheus 定时 Pull 存储 → Grafana 可视化 → Alertmanager 告警。
记住一个关键词:Pull(拉取)
Prometheus 是 主动定时去拉 各应用的指标端点(默认 15s 一次),而不是应用往它推。所以应用这边只需要「把指标挂在一个 HTTP 地址上」,剩下的交给 Prometheus 定时来取。
四个角色各司其职:
| 角色 | 职责 | 类比 |
|---|---|---|
| Micrometer / JMX Exporter | 把 JVM 内部状态翻译成标准指标格式 | 体检仪器 |
| Prometheus | 定时抓取、存成时间序列、提供查询 | 病历档案库 |
| Grafana | 把时序数据画成直观的图表 | 体检报告单 |
| Alertmanager | 指标越线时发通知 | 异常预警铃 |
二、第一步:把 JVM 指标暴露出来
指标来源有两条主流路线,按你的应用类型二选一。
2.1 Spring Boot 应用:Micrometer + Actuator(推荐)
Spring Boot 自带的 Actuator 集成了 Micrometer(号称「监控界的 SLF4J」),只要加依赖就能自动采集全套 JVM 指标。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
再在 application.yml 里把 Prometheus 端点放出来:
management:
endpoints:
web:
exposure:
include: health,info,prometheus # 暴露 /actuator/prometheus
metrics:
tags:
application: ${spring.application.name} # 给所有指标打上应用名标签,多实例区分
启动后访问 http://localhost:8080/actuator/prometheus,你会看到一大坨形如 jvm_memory_used_bytes{area="heap",...} 1.234E8 的文本 —— 这就是 Prometheus 能读懂的指标。
生产别裸暴露 Actuator 端点
/actuator/prometheus(乃至env、heapdump等)会泄露内部指标与配置,绝不能直接暴露到公网。生产环境至少做到一条:
- 只放内网:把 Actuator 端口与业务端口分开(
management.server.port),只让 Prometheus 所在内网 / 安全组能访问;- 加鉴权:用 Spring Security 给
/actuator/**加上认证;- 最小暴露:
exposure.include只列真正需要的端点,别图省事写*。
2.2 非 Spring 应用:JMX Exporter
老项目或非 Spring 的应用,可以挂一个 JMX Exporter 的 Java Agent,它把 JVM 的 JMX MBean 转成 Prometheus 格式:
java -javaagent:/path/jmx_prometheus_javaagent.jar=9404:config.yaml -jar app.jar
启动后指标挂在 http://localhost:9404/metrics。
两条路线的指标名不一样
这是踩坑重灾区:Micrometer 和 JMX Exporter 暴露的指标命名不同,导入 Grafana 看板时要对应清楚。
含义 Micrometer JMX Exporter 堆已用 jvm_memory_used_bytesjvm_memory_bytes_usedGC 耗时 jvm_gc_pause_secondsjvm_gc_collection_seconds本文后续 PromQL 一律以 Micrometer 命名为准(Spring Boot 场景最常见)。
三、第二步:Prometheus 抓取配置
在 prometheus.yml 里加一个抓取任务,告诉它去哪、多久拉一次:
scrape_configs:
- job_name: 'jvm-app'
metrics_path: '/actuator/prometheus' # Micrometer 端点路径
scrape_interval: 15s
static_configs:
- targets: ['10.0.0.11:8080', '10.0.0.12:8080'] # 多个实例
reload 后在 Prometheus 的 Status → Targets 页面看到对应 target 是 UP,就说明数据通了。
验证数据已入库
在 Prometheus 查询框输入
jvm_memory_used_bytes,能出曲线就成功了。看不到先排查三处:应用端点能否访问、target 是否 UP、防火墙 / 网络是否放行。
四、第三步:Grafana 看板
不用从零画。Grafana 官方市场有现成的 JVM 看板,直接导入 ID 即可:
| 看板 ID | 适用 | 说明 |
|---|---|---|
| 4701 | Micrometer | 最经典的「JVM (Micrometer)」看板,面板齐全 |
| 11378 | Micrometer | Spring Boot 2.1+ 增强版 |
| 8563 | JMX Exporter | 配合 JMX Exporter 使用 |
操作:Grafana → Dashboards → Import → 输入 4701 → 选择 Prometheus 数据源 → Import。
导入后通常还要在看板顶部选对 application / instance 变量,才能筛到你的服务。
4.1 十分钟本地跑通(docker-compose 最小栈)
想自己复现下面这张看板?这套最小栈(Prometheus + Grafana,指向你本机已开 Actuator 的应用)十分钟就能起来。
新建 prometheus.yml:
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'jvm-app'
metrics_path: '/actuator/prometheus'
static_configs:
# host.docker.internal 让容器访问宿主机上的应用
- targets: ['host.docker.internal:8080']
再建 docker-compose.yml:
services:
prometheus:
image: prom/prometheus:latest
ports: ["9090:9090"]
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
extra_hosts: ["host.docker.internal:host-gateway"] # Linux 需要这行
grafana:
image: grafana/grafana:latest
ports: ["3000:3000"]
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
三步跑通:
docker compose up -d起 Prometheus(:9090)+ Grafana(:3000);- 打开
http://localhost:9090/targets,确认jvm-app是UP(不 UP 回头看第三章排查三处); - 打开
http://localhost:3000(admin / admin)→ 添加数据源 Prometheus(URL 填http://prometheus:9090)→ 按上面导入看板 4701 → 顶部选对application变量。
看到堆的锯齿开始跳动,整条链路就通了 —— 下面第五章就是教你读这张图。
五、读懂看板:面板含义 × 实战数据
理论干讲容易记不住。这里换个方式:直接拿一台真实在线服务的看板(4701)自上而下逐区段拆解。每个区段先看图,再用一张表把 「这张图在说什么(含义 + 底层指标 + 正常形态)」和「本例的真实读数」并排呈现,最后给出健康判断 —— 含义和数据对照着看,一遍就懂。
(常见异常现象「长什么样、怎么处理」,单独抽到 第六章 做成速查表。)
先亮这台机器的「底牌」
- 宿主规格:8 vCPU / 16 GiB 内存
- 堆上限:
-Xmx = 10 GiB(committed 也已顶到 10 GiB)- 已运行:3 天(Start time
2026-06-17 17:47:10)一句话先记住:堆给了 10G、机器总共才 16G。堆 + 非堆 + 堆外 + 操作系统要挤在 16G 里,「堆用了多少」要时刻和 16G 物理内存挂钩着看。
没空全看?先抓这 3 张
时间紧时,盯这三张就能给系统定性八成:
- Quick Facts(5.1) —— 30 秒看 uptime + 堆 / 非堆水位;
- 堆内存池的 Old Gen(5.5) —— 判断有没有内存泄漏的「金标准」;
- GC 的 Pause Durations(5.7) —— 直接关系用户体感的停顿。
其余面板,是定性发现异常后「顺藤摸瓜」时才逐张细看的。
赶时间的话,先看这张「9 个区段 × 一句话结论」总表,再挑感兴趣的钻进去:
| 区段 | 一句话看什么 | 本例结论 |
|---|---|---|
| 5.1 Quick Facts | uptime + 堆 / 非堆水位 | Heap 76%(橙)需下探,其余 OK |
| 5.2 I/O Overview | 业务压力(QPS / 状态码 / 耗时) | ~1.3K QPS 健康,但有「小时级」长尾 |
| 5.3 JVM Memory | 堆是不是「能回落的锯齿」 | 是健康锯齿;committed=max=10G,预算偏紧 |
| 5.4 JVM Misc | CPU / 线程 / 日志 | CPU 三成、blocked≈0;日志量惊人 |
| 5.5 堆内存池 | 老年代趋势(泄漏金标准) | 老年代 1.85/10G 平直 → 无泄漏 |
| 5.6 非堆内存池 | Metaspace 有没有爬坡 | 平直健康;No data 是查询坑 |
| 5.7 垃圾回收 | GC 频率 / 停顿 / 晋升 | Young GC 约 10s 一次、停顿 25–50ms、晋升≈0 |
| 5.8 类加载 | 类数是否只增不减 | 约 25K 平直 → 健康 |
| 5.9 Buffer Pools | 堆外内存是否失控 | ~286MiB 平稳,计入内存预算 |
5.1 Quick Facts:30 秒抓住整体状态

顶部四个 Stat 面板是整张看板的「体检首页」:
| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Uptime | 进程连续运行时长 | 3.0 天 | 没崩溃 / 没频繁重启 |
| Start time | 本次启动时刻 | 2026-06-17 17:47:10 | 排障时可对齐发布 / 重启时间 |
| Heap used | 堆已用 ÷ -Xmx | 76.49%(橙) | 偏高、被标成警示色,需往下查 |
| Non-Heap used | 元空间 + Code Cache 等使用率 | 21.34%(绿) | 很宽裕 |
判断:先别慌,但要往下查
Non-Heap 绿色、uptime 够长,这两项没问题。真正扎眼的是 Heap used 76.49%(橙色) —— 单看这个数字会以为「堆快满了、要 OOM 了」。 但 Stat 面板只是此刻的快照,最会骗人:它可能正好卡在某次 Young GC 之前、Eden 快被填满的瞬间。到底是「真的逼近上限」还是「Eden 涨上去马上会被回收」,必须看 5.3 的趋势图和 5.5 的分代拆解才能下结论(剧透:这 76% 主要是 Eden 撑起来的,老年代其实很空)。
5.2 I/O Overview:先看「业务压力」有多大
这一段不是 JVM 内部指标
Rate / Http Code / Duration 这些是 应用层的 HTTP 流量(来自 Micrometer 的
http.server.requests)。4701 看板把它放在最前面是有讲究的 —— 先知道服务正承受多大压力,再去对照 JVM 的反应,两者要结合着看。

| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Rate | 总请求速率 | 1.35K ops/s(1K~2.5K 波动) | 平稳、有昼夜节律 |
| Http Code | 按状态码分布 | 204≈1.29K、200≈52.5、4xx≈9.77 ops/s,无 5xx | 成功率高(204 = 成功但无返回体,多为上报 / 心跳) |
| Duration | 响应耗时 | 200-AVG 14.8 ms、200-MAX 61.2 ms、HTTP-MAX 209 ms;纵轴顶到 2.78 小时、有一根冲天尖刺 | 日常很快,但有 极端长尾 |
| AE Req/m | 每分钟请求量 | 昼夜波峰(峰值约 2.5 万);TRY 10920、Process 6527 | 规律波动 |
判断:吞吐健康,但有「极端长尾」要追
1.3K QPS、几乎无错误码,承压正常。两个要点:
- 那根冲到「小时级」的 Duration 尖刺 不能放过 —— 把它的时间点与 5.7 的 GC「Pause Durations」、5.4 的 Log Events 尖峰、5.3 的堆锯齿对齐,判断是 GC、慢查询还是某个批处理卡住;
- 流量有昼夜节律,后面看 GC / Eden 时要记得:波形跟着流量走是正常的,不是异常。
这正是把 I/O 和 JVM 放进同一张看板的价值:业务现象 ↔ JVM 内因,一眼对照。
怎么追那根「小时级」长尾
AVG / MAX 都会骗人:AVG 被海量快请求拉低、显得很美好;MAX 又只是孤零零一个点。真正该盯的是 P99 / P999 分位(Micrometer 的
http_server_requests_seconds直方图 + PromQLhistogram_quantile(0.99, ...))。而要定位「具体是哪一笔请求卡了一小时」,得靠 分布式追踪(TraceId)或慢请求日志 —— 光看这张聚合图抓不到那一笔。
5.3 JVM Memory:堆的「锯齿」是不是健康的

最该盯最左的 JVM Heap。底层指标:jvm_memory_used_bytes{area="heap"}(已用)、jvm_memory_committed_bytes(已申请)、jvm_memory_max_bytes(上限 -Xmx)。正常形态是一条「锯齿线」:对象不断分配缓慢爬升 → 一次 GC 拉回低位 → 周而复始。
| 线 / 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Heap · used(绿) | 真正装了对象的量,呈锯齿 | Max 8.12 / Current 7.49 GiB | 在 ~4.66~9.31 GiB 间剧烈锯齿 |
| Heap · committed(黄) | 已向 OS 申请到手的堆 | 10 GiB | 已顶满 |
| Heap · max(蓝) | 上限 -Xmx | 10.00 GiB | 天花板 |
| JVM Non-Heap | 元空间 / Code Cache 等 | used 270 MiB / max 1.23 GiB | 一条水平线(详见 5.6) |
| JVM Total | 堆 + 非堆 | used 7.75 GiB / committed 10.3 GiB | 跟堆同步锯齿 |
| JVM Process Memory | 进程总内存 | No data | 查询标签不匹配(同 5.6) |
常用 PromQL —— 堆使用率:
sum(jvm_memory_used_bytes{area="heap"}) by (instance)
/
sum(jvm_memory_max_bytes{area="heap"}) by (instance)
三条线怎么看:
used ≤ committed ≤ max(以及-Xms的影响)用「公司租停车位」理解:max = 物业允许你公司 最多能租 的车位(
-Xmx= 10 GiB);committed = 你 已签约租下、画上编号占住 的车位(哪怕空着别人也停不进来);used = 此刻 真正停了车 的车位(车不停开进开出 = 对象分配与 GC 回收,所以它呈锯齿)。本例 committed 已等于 max(都是 10 GiB):堆已扩张到最大且不再缩回,要么
-Xms = -Xmx启动即占满、要么运行中一路顶上来。这不是坏事(省去反复向 OS 要内存),但意味着 这 10 GiB 基本被进程占住,正是要算「16 GiB 整机内存预算」的原因。(注:committed 是 OS 承诺给 JVM 的额度,未必时刻是物理驻留,除非开了-XX:+AlwaysPreTouch。)能不能在图上看到 committed 这条独立中间线,取决于
-Xms:设-Xms = -Xmx时黄线(committed)与蓝线(max)完全重合、只剩绿线 used 锯齿(本例即是);设-Xms < -Xmx(如2G / 10G)时 committed 从低起步、随 used 阶梯式扩容,才会看到used < committed < max三线分离。
判断:锯齿本身是健康的,但要警惕「内存预算」
好消息:used 是 能大幅回落的锯齿(一路能跌回 ~4.66 GiB),不是「只涨不回的爬坡」,说明 GC 在有效回收、没有典型泄漏特征。5.1 那个吓人的 76% 主要是这条锯齿的高点,Eden 涨满的瞬间被快照逮到了而已(详见 5.5)。 需要盯住的是 物理内存预算:
committed已经把 10 GiB 堆全额锁定,加上非堆 ~0.3 GiB、堆外 ~0.3 GiB,进程占用逼近 11 GiB,而整机只有 16 GiB —— 留给操作系统和 page cache 的余量已经不算宽裕。堆配置可以说「踩在偏激进的线上」,没问题但没多少冗余。
5.4 JVM Misc:CPU、线程与日志

底层指标:process_cpu_usage / system_cpu_usage、jvm_threads_live_threads / _daemon_threads / _peak_threads、jvm_threads_states_threads{state="..."}。
| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| CPU Usage | 进程 / 整机 CPU 占用 | system 31.78%(max 54.38%)、process 34.59% | 8 核只用三成多,没吃满 |
| Load | 系统负载 vs 核数 | cpus=8、system-1m 3.50(max 8.61) | 平时轻松,峰值触及核数 |
| Threads | 存活 / 守护 / 峰值线程 | live 1.3K、daemon 617、peak 1.3K | 平线,3 天无增长(无泄漏) |
| Thread States | 按状态分布 | blocked 0、runnable ≈ 449,余在 waiting | 无锁争用 / 死锁 |
| Log Events | 每分钟各级别日志条数 | info 3.4M ops/m(峰 6.4M)、warn 267.3K、error 1.3 | 日志量惊人 |
| File Descriptors | 打开句柄 / 上限 | open 2.3K / max 1.0 Mil | 离上限很远 |
读懂 CPU Usage 的三条线:system / process / process-1h
底层只有两个指标,
-1h是给 process 套了「1 小时窗口」算的平滑基线:
图例 底层指标 含义 回答的问题 system system_cpu_usage整台机器 所有进程的 CPU 占用 这台 8 核宿主忙不忙? process process_cpu_usage只算当前 JVM 进程 的 CPU 占用 我的应用自己吃多少 CPU? process-1h process_cpu_usage的 1 小时平均process 线的 趋势基线(滤掉瞬时毛刺) 最近一小时是持续高,还是偶尔抖? 两个关键认知:
- 按核归一化:这俩值都已除以核数,0–100% 是 整机口径。本例
process 34.59%在 8 vCPU 上 ≈8 × 0.3459 ≈ 2.8 个核满载,而非「一个核用了 34%」—— 所以要和 Load(cpus=8、峰值 8.61)对照着看。- system vs process 偶尔倒挂属正常:本例 process 34.59% 略高于 system 31.78%,是因为两者由 不同 OS 计数器、各自独立采样 得出,瞬间不完全对齐 —— 差几个点是噪声。只有 持续、大幅 倒挂才要怀疑(多为容器 / cgroup 环境下两者基准不一致)。
排障口诀:process 高 + system 高 → 我的应用在吃 CPU(
top -Hp→jstack);system 高 + process 低 → CPU 被别的进程抢了;process-1h 一路抬升 → 是持续恶化趋势,不是抖动。
判断:CPU / 句柄健康、无锁争用;两点要管
好信号:CPU 不紧张、blocked ≈ 0(几乎没有锁竞争 / 死锁)、FD 充裕、线程平线无泄漏。 两点要留意:
- 日志量惊人:info 每分钟 300 多万行(峰值 640 万)。海量日志会 疯狂创建字符串对象,这极可能就是 5.5 里 Eden 被打满、5.7 里分配速率高达 ~1 GB/s 的 幕后推手。建议把生产日志级别从 INFO 收敛到 WARN,既减 I/O 又直接降 GC 压力;
- Load 峰值触及核数:平时富余,但高峰会短暂打满 8 核,结合 5.2 的昼夜流量节律,需确认峰值时段延迟是否达标。
5.5 堆内存池:Eden / Old / Survivor(全场最关键)

这一节是揭开 5.1「76%」谜底的关键。把堆按内存池拆成三张图,正好对应 JVM 系列·核心篇 讲的分代模型:
| 内存池 | 对应分代 / 含义 | 本例实测 | 解读 |
|---|---|---|---|
| G1 Eden Space | 年轻代 Eden,新对象出生地 | used 5.49 GiB(committed 6.29、峰值 ~6.98) | 高频锯齿,涨满即被 Young GC 清空 |
| G1 Old Gen | 老年代,长寿对象 —— 泄漏看这里 | used 1.85 GiB / max 10 GiB | 低位平直,3 天不涨 |
| G1 Survivor Space | 年轻代幸存区 | used 156 MiB(峰值 428) | 周期方块,每轮 GC 涨一块再回落 |
判断:谜底揭晓 —— 76% 是 Eden 撑起来的「虚高」
把三块加起来:Eden 5.49 + Old 1.85 + Survivor 0.16 ≈ 7.5 GiB,正好等于 5.3 的堆 Current 7.49 GiB。其中 5.49 GiB 是随时会被 Young GC 清空的 Eden。 所以 5.1 那个橙色 76% 的真相是:快照恰好抓在 Eden 快填满的高点,而真正衡量「内存压力」的 老年代只用了 1.85 / 10 GiB(≈18%)、且 3 天平直不涨 —— 这才是「没有泄漏、内存健康」最硬的证据。 教训:永远别用「Heap used 百分比」单独下结论,要拆到老年代趋势才算数。
5.6 非堆内存池:Metaspace 有没有泄漏

先搞懂看板上的「Non-Heap」是什么
一句话:堆装你
new出来的对象,非堆装 JVM 自己运转所需的基础设施 —— 主要是 Metaspace(类的元信息,即方法区) 和 Code Cache(JIT 编译出的机器码)。它们一次加载好就长期不变,所以非堆曲线通常是 接近水平的直线,不像堆那样剧烈锯齿(详见 JVM 系列·入门篇)。
底层指标:jvm_classes_loaded_classes、jvm_memory_used_bytes{area="nonheap", id="Metaspace"}。
| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Metaspace | 类元信息(方法区) | used 153 MiB、committed 179、max -1(不限) | 水平线,无类爆炸 |
| Compressed Class Space | 压缩类指针空间 | used 16.0 MiB / max 1 GiB | 低平线 |
| CodeHeap ×3 | JIT 机器码缓存(分段) | No data | 查询标签不匹配 |
判断:Metaspace 健康;「No data」是看板的坑、不是故障
Metaspace 153 MiB 平直 → 没有反射 / 动态代理 / 热部署导致的类爆炸(若它 只增不减、不断爬坡,就要警惕,终点是元空间 OOM)。 那三个「No data」的分段 Code Cache 面板,是 看板 PromQL 与当前 JDK 的指标标签对不上(分段 Code Cache 指标名各 JDK 有差异),属于 2.2 提到的「命名不一致」坑 —— 修查询即可,与 JVM 本身无关。
5.7 垃圾回收:G1 跑得怎么样

底层指标:jvm_gc_pause_seconds(Timer,含 _count / _sum / _max,带 cause 标签)、jvm_gc_memory_allocated_bytes_total、jvm_gc_memory_promoted_bytes_total。
| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Collections | GC 频率 / 类型 | ~0.1 ops/s(约 10s 一次),全 G1 Evacuation(Young) | 不频繁、无 Full GC |
| Pause Durations | 单次停顿时长 | 多在 25~50 ms,峰值 ~75~100 ms | 在线服务可接受 |
| Allocated / Promoted | 分配 / 晋升速率 | allocated 0.5~1 GB/s,promoted ≈ 0 | 对象死在年轻代 |
常用 PromQL(rate(x[1m]) = 按最近 1 分钟窗口算 x 的每秒平均增量,是把「累计计数器」换算成「速率」的标准手法):
# 平均单次 GC 停顿(秒)= 这段时间总停顿 ÷ 总次数
rate(jvm_gc_pause_seconds_sum[1m]) / rate(jvm_gc_pause_seconds_count[1m])
# 分配速率 / 晋升速率(字节每秒)
rate(jvm_gc_memory_allocated_bytes_total[1m])
rate(jvm_gc_memory_promoted_bytes_total[1m])
判断:G1 工作正常,停顿在可接受区间
因果链:分配速率高 → Eden 填满快 → Young GC 频繁;晋升速率高 → 老年代涨得快 → 早晚 Full GC。本例「分配高、晋升 ≈ 0」正说明对象死在年轻代(也解释了 5.5 老年代为何只有 1.85 GiB 平直)。停顿多在几十毫秒,在线服务通常希望单次 < 100ms(对延迟敏感的金融 / 游戏追求 < 10ms,那是 ZGC 的主场)。 两条优化线索:① 5.4 提到的海量日志正是这 ~1 GB/s 分配率的主要来源,降日志 = 降分配 = 降 GC 频率;② 若 5.2 那根极端 Duration 长尾要彻底排除 GC 嫌疑,可把这里的 Pause 时间轴和长尾时间点对齐核对,或调
MaxGCPauseMillis/ 评估 ZGC。
5.8 类加载:类的数量稳住了吗

| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Classes loaded | 当前已加载类数 | 约 25K 条水平线 | 启动加载完就稳定,3 天没爬升 |
| Class delta | 每分钟加载 / 卸载类数 | 基本 0,仅启动初期有 +25 / -75 | 之后再无 churn |
判断:健康
若
loaded这条线 持续往上爬,要警惕反射 / CGLIB / 热部署不停造类(呼应 5.6,终点是 Metaspace OOM)。这里启动后即平直、delta 归零,正常。
5.9 Buffer Pools:堆外内存有没有失控

Buffer Pools 反映的是 JVM 堆之外 的直接内存使用,主要来自 NIO / Netty:
| 面板 | 含义 | 本例实测 | 解读 |
|---|---|---|---|
| Direct Buffers(used) | NIO / Netty 直接内存用量 | ~286 MiB 平直线(used ≈ capacity) | 稳定不增长 |
| Direct Buffers(count) | 缓冲个数 | 约 1.5K 个 | 平稳 |
| Utilisation | 使用率 | No data | 查询标签不匹配(同 5.6) |
| Mapped Buffers | 内存映射文件(mmap) | 0 | 本服务没用 mmap |
判断:正常,但要计入「内存预算」
直接内存 ~286 MiB 平稳不增长,NIO / Netty 堆外缓冲健康。
两点提醒:① 这块 不受
-Xmx限制,泄漏时堆看着没事、进程却被系统 OOM Kill,所以 Direct Buffers 若 只增不减,要排查DirectByteBuffer没释放(呼应 JVM 系列·核心篇 讲的虚引用管理堆外内存);② 把它的 ~0.3 GiB 计进 5.3 的内存预算:堆 10 GiB + 非堆 0.3 + 堆外 0.3 ≈ 10.9 GiB,逼近 16 GiB 整机的七成。
5.10 一句话体检结论
这台 JVM(8 vCPU / 16 GiB,-Xmx 10 GiB,已跑 3 天)健康吗?—— 基本健康,但有几处要管。
- 「76% 堆」是虚惊:拆开看是 6 GiB 大 Eden 涨满的瞬间被快照逮到,老年代仅 1.85 / 10 GiB 且 3 天平直 —— 没有泄漏;
- GC 健康:约 10 秒一次纯 Young GC、停顿 25~50 ms、晋升 ≈ 0(对象死得早);
- 运行态稳:CPU ~32%、Load 平时 3.5、blocked ≈ 0(无锁争用)、线程 / 类 / 直接内存均平直、FD 充裕;
- 业务侧:~1.3K QPS、几乎全 2xx / 204;
待办(按优先级):
- 海量日志:info 每分钟 300+ 万行 —— 这是 ~1 GB/s 分配率和 Eden 高频锯齿的主因,收敛日志级别可一举降低 I/O 与 GC 压力;
- 极端长尾:5.2 那根冲到「小时级」的 Duration 尖刺要定位(与 GC / 慢任务时间点对齐);
- 内存预算偏紧:进程 ~10.9 GiB 占 16 GiB 整机近七成,留给 OS / page cache 余量不多,扩容或微调
-Xmx前先评估;- Load 峰值触及 8 核:高峰短暂跑满,确认峰值延迟达标;
- 看板修缮:CodeHeap / Process Memory / Utilisation 多处「No data」,是 PromQL 与本 JDK 标签不匹配,修查询即可。
六、常见现象的分析与处理
第五章是「正常看板长什么样」。这一章反过来:列出几类典型的「异常形态」,对应大概率原因和下一步动作,当成线上排障速查表用。每一行都能对应回第五章的某张面板。
| 看板上的现象 | 大概率原因 | 下一步动作 |
|---|---|---|
| 老年代基线台阶式上涨、GC 拉不回(看 5.5 Old Gen) | 内存泄漏 | jmap 导 heap dump,用 MAT 看支配树 |
| Full GC 频繁、晋升速率高(看 5.7) | 大对象 / Survivor 太小 / 过早晋升 | 调 -Xmn、SurvivorRatio,加 -XX:+PrintTenuringDistribution |
| Metaspace 与类数持续爬坡(看 5.6 / 5.8) | 动态生成类太多 | 排查反射 / CGLIB / 热部署 |
| 线程数只增不减(看 5.4) | 线程泄漏 | jstack 看线程,检查线程池配置 |
大量 BLOCKED 线程(看 5.4 Thread States) | 锁竞争 / 死锁 | jstack 找 BLOCKED 链,对照 JVM 系列·进阶篇 的并发内存模型 |
| GC 时间占比高、堆频繁满(看 5.3 / 5.7) | 堆太小 / 分配速率过高 | 加大 -Xmx,或优化高频对象分配(如降日志) |
process_cpu_usage 飙高(看 5.4) | 热点代码 / 死循环 | top -Hp 定位线程 → jstack / 火焰图 |
| Direct Buffers 只增不减(看 5.9) | 堆外内存泄漏 | 排查 DirectByteBuffer 未释放,留意进程被 OOM Kill |
| 响应长尾但堆 / GC 正常(看 5.2 vs 5.7) | 慢查询 / 外部依赖 / 批处理 | 对齐时间点排查,而非一味调 JVM |
监控的真正价值
它不替你解决问题,但它让你 第一时间发现问题、并把范围缩小到某一类。「度量 → 分析 → 调整 → 验证」这个 JVM 系列·进阶篇 讲的调优闭环,监控就是其中「度量」那一环的线上版本。
6.1 配告警时的「参考水位」
把上表里最关键的几条写成 Prometheus 告警规则,就能在出事前自动通知、不用人盯着看板。下面是一份起步用的阈值参考(务必结合自家业务基线微调):
| 指标 | 预警(warning,趋势性) | 危急(critical,已影响) |
|---|---|---|
| 老年代使用率 | 持续 > 70% | 持续 > 90% |
| 平均 GC 停顿 | > 100 ms | > 500 ms |
| Full GC 频率 | 偶发 | 分钟级频发 |
| Metaspace | 缓慢上涨 | 持续上涨 / 逼近上限 |
| 线程数 | 缓慢上涨 | 只增不减、逼近 ulimit |
| 文件句柄 | > 50% 上限 | > 80% 上限 |
| Direct Buffers | 缓慢上涨 | 只增不减 |
| process CPU | 持续 > 70% | 持续 ≈ 100% |
告警别配成噪音
三条原则:① 给够
for(如for: 5m)滤掉瞬时毛刺;② 分级 —— warning 是「趋势预警、可白天处理」,critical 是「已影响、要立刻响应」,走不同通知渠道;③ 每条告警都对应到上面对照表里的一个动作,收到就知道该干嘛,否则迟早被当噪音忽略。
落到可直接复制的规则,长这样(Prometheus rule 文件格式,以老年代、GC 停顿两条为例):
groups:
- name: jvm-alerts
rules:
# 老年代使用率持续偏高 —— 疑似泄漏或堆偏小(对应 §6 速查表第 1 行)
- alert: JvmOldGenHigh
expr: |
sum by (instance) (jvm_memory_used_bytes{area="heap", id=~".*Old.*"})
/ sum by (instance) (jvm_memory_max_bytes{area="heap", id=~".*Old.*"})
> 0.9
for: 5m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} 老年代使用率 > 90% 持续 5m"
description: "疑似内存泄漏或堆偏小,建议 jmap 导 dump + MAT 分析支配树"
# 平均单次 GC 停顿过长 —— 直接影响用户体感延迟(对应速查表 GC 行)
- alert: JvmGcPauseHigh
expr: |
rate(jvm_gc_pause_seconds_sum[5m]) / rate(jvm_gc_pause_seconds_count[5m])
> 0.1
for: 10m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} 平均 GC 停顿 > 100ms"
一个细节:id=~".*Old.*" 用正则匹配老年代池名,是因为不同收集器名字不同(G1 是 G1 Old Gen、Parallel 是 PS Old Gen)—— 这正是 2.2「命名不一致」在告警里的翻版。
七、小结
把这条链路浓缩成一句话:
全文脉络回顾
- 暴露:Spring Boot 用 Micrometer + Actuator,其余用 JMX Exporter,把 JVM 状态挂成 HTTP 指标端点;
- 采集:Prometheus 定时 Pull、存成时序数据;
- 可视化:Grafana 导入现成看板(如 4701),自上而下盯 I/O、堆与分代、GC、线程、类、堆外等核心面板;
- 解读:健康的堆是「能回到低基线的锯齿」,重点警惕老年代爬坡、Full GC 频繁、线程 / 类只增不减。
从第五章那次实战拆解里,提炼出三条最该记住的「读图铁律」:
看板解读三条铁律
- 别被单个百分比吓到:
Heap used 76%看着危险,拆到分代才发现是 Eden 的「虚高」、老年代很空。判断内存健康,永远看老年代趋势,而不是堆总量百分比。- 业务指标和 JVM 指标要对照着看:响应慢、长尾,先把 I/O 的时间点和 GC 停顿、堆锯齿、日志尖峰对齐 —— 把「业务现象」对到「JVM 内因」上。
No data多半是查询问题,不是故障:面板空白通常是 PromQL 与当前 JDK 的指标标签对不上(见 2.2),先怀疑看板、别怀疑 JVM。
原理 → 监控 → 诊断,三步连起来
《JVM 深入》系列教你 JVM 内部怎么运作(知其所以然),这一篇教你 在线上怎么远远看见它的运作(出了事能预警、能缩小范围)。当看板把你领到某台机器门口、却答不上「具体是哪段代码」时,就该轮到 Arthas 这类在线诊断工具 登场 —— 不重启、不改代码地钻进去做现场探查。原理 → 监控 → 诊断,三者合起来才是从「会写 Java」到「扛得住线上」的完整闭环。
延伸阅读
- Grafana Labs. JVM (Micrometer) dashboard · ID 4701 —— 本文逐区段拆解的看板本体。
- Micrometer. Micrometer Prometheus registry —— Spring Boot 指标暴露的官方说明。
- Prometheus. Configuration · scrape_configs —— 抓取任务与告警规则配置参考。
- Prometheus. Querying · functions(rate / histogram_quantile) —— 本文 PromQL 用到的核心函数。