Skip to content
Charles Shao
Go back

Grafana JVM 监控分析:从指标采集到看板解读的实战指南

views

上一系列《JVM 深入》(共三篇)讲清了 JVM 内部「长什么样、怎么干活」。这一篇解决另一半问题 —— 线上运行时,怎么实时看见它的状态、并据此定位问题

知道了 JVM 有堆、有 GC、有线程,可线上服务半夜内存飙高、Full GC 频发的时候,你总不能一台台机器去敲 jstat

真正的工程做法是:让每个 JVM 持续把自己的状态「播报」出来,集中采集、画成图、设好告警。这套最主流的组合就是 Micrometer / JMX Exporter(暴露指标)+ Prometheus(采集存储)+ Grafana(可视化)。这篇我们就把这条链路从头到尾走一遍,重点落在 每张图到底在说什么、怎么从图里看出问题

TL;DR

Table of contents

Open Table of contents

一、先看全景:数据怎么从 JVM 流到 Grafana

在动手之前,先建立一张「数据流动图」,后面所有配置都是在填充这张图的某一环:

JVM 监控数据流动图:你的 Java 应用里 JVM 运行时(堆 / GC / 线程 / 类)把状态送到指标暴露端点(Micrometer 或 JMX Exporter),端点通过 HTTP /actuator/prometheus 被 Prometheus 定时拉取、抓取并存成时序数据;Prometheus 再经 PromQL 查询把数据交给 Grafana 做看板可视化与告警,并按规则触发 Alertmanager 发出告警通知 监控链路全景:应用把指标挂成 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(乃至 envheapdump 等)会泄露内部指标与配置,绝不能直接暴露到公网。生产环境至少做到一条:

  • 只放内网:把 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 看板时要对应清楚。

含义MicrometerJMX Exporter
堆已用jvm_memory_used_bytesjvm_memory_bytes_used
GC 耗时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适用说明
4701Micrometer最经典的「JVM (Micrometer)」看板,面板齐全
11378MicrometerSpring Boot 2.1+ 增强版
8563JMX 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

三步跑通:

  1. docker compose up -d 起 Prometheus(:9090)+ Grafana(:3000);
  2. 打开 http://localhost:9090/targets,确认 jvm-appUP(不 UP 回头看第三章排查三处);
  3. 打开 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 张

时间紧时,盯这三张就能给系统定性八成:

  1. Quick Facts(5.1) —— 30 秒看 uptime + 堆 / 非堆水位;
  2. 堆内存池的 Old Gen(5.5) —— 判断有没有内存泄漏的「金标准」;
  3. GC 的 Pause Durations(5.7) —— 直接关系用户体感的停顿。

其余面板,是定性发现异常后「顺藤摸瓜」时才逐张细看的。

赶时间的话,先看这张「9 个区段 × 一句话结论」总表,再挑感兴趣的钻进去:

区段一句话看什么本例结论
5.1 Quick Factsuptime + 堆 / 非堆水位Heap 76%(橙)需下探,其余 OK
5.2 I/O Overview业务压力(QPS / 状态码 / 耗时)~1.3K QPS 健康,但有「小时级」长尾
5.3 JVM Memory堆是不是「能回落的锯齿」是健康锯齿;committed=max=10G,预算偏紧
5.4 JVM MiscCPU / 线程 / 日志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 秒抓住整体状态

Grafana 4701 看板顶部 Quick Facts 概览:四个 Stat 面板并排显示 Uptime 3.0 天、Start time 2026-06-17 17:47:10、Heap used 76.49%(橙色警示)、Non-Heap used 21.34%(绿色)

顶部四个 Stat 面板是整张看板的「体检首页」:

面板含义本例实测解读
Uptime进程连续运行时长3.0 天没崩溃 / 没频繁重启
Start time本次启动时刻2026-06-17 17:47:10排障时可对齐发布 / 重启时间
Heap used堆已用 ÷ -Xmx76.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 的反应,两者要结合着看。

Grafana 4701 看板 I/O Overview 区段:展示 HTTP 流量的请求速率 Rate、按状态码分布 Http Code、响应耗时 Duration(含一根冲天长尾尖刺)与每分钟请求量 AE Req/m 曲线

面板含义本例实测解读
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、几乎无错误码,承压正常。两个要点:

  1. 那根冲到「小时级」的 Duration 尖刺 不能放过 —— 把它的时间点与 5.7 的 GC「Pause Durations」、5.4 的 Log Events 尖峰、5.3 的堆锯齿对齐,判断是 GC、慢查询还是某个批处理卡住;
  2. 流量有昼夜节律,后面看 GC / Eden 时要记得:波形跟着流量走是正常的,不是异常。

这正是把 I/O 和 JVM 放进同一张看板的价值:业务现象 ↔ JVM 内因,一眼对照。

怎么追那根「小时级」长尾

AVG / MAX 都会骗人:AVG 被海量快请求拉低、显得很美好;MAX 又只是孤零零一个点。真正该盯的是 P99 / P999 分位(Micrometer 的 http_server_requests_seconds 直方图 + PromQL histogram_quantile(0.99, ...))。而要定位「具体是哪一笔请求卡了一小时」,得靠 分布式追踪(TraceId)或慢请求日志 —— 光看这张聚合图抓不到那一笔。

5.3 JVM Memory:堆的「锯齿」是不是健康的

Grafana 4701 看板 JVM Memory 区段:左侧 JVM Heap 呈现 used(绿,锯齿)、committed(黄)、max(蓝)三条线,右侧是 JVM Non-Heap、JVM Total 与 JVM Process 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(蓝)上限 -Xmx10.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、线程与日志

Grafana 4701 看板 JVM Misc 区段:包含 CPU Usage(system / process / process-1h 三条线)、Load、Threads、Thread States、Log Events 与 File Descriptors 面板

底层指标:process_cpu_usage / system_cpu_usagejvm_threads_live_threads / _daemon_threads / _peak_threadsjvm_threads_states_threads{state="..."}

面板含义本例实测解读
CPU Usage进程 / 整机 CPU 占用system 31.78%(max 54.38%)、process 34.59%8 核只用三成多,没吃满
Load系统负载 vs 核数cpus=8system-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 小时窗口」算的平滑基线:

图例底层指标含义回答的问题
systemsystem_cpu_usage整台机器 所有进程的 CPU 占用这台 8 核宿主忙不忙?
processprocess_cpu_usage只算当前 JVM 进程 的 CPU 占用我的应用自己吃多少 CPU?
process-1hprocess_cpu_usage 的 1 小时平均process 线的 趋势基线(滤掉瞬时毛刺)最近一小时是持续高,还是偶尔抖?

两个关键认知:

  • 按核归一化:这俩值都已除以核数,0–100% 是 整机口径。本例 process 34.59% 在 8 vCPU 上 ≈ 8 × 0.3459 ≈ 2.8 个核 满载,而非「一个核用了 34%」—— 所以要和 Loadcpus=8、峰值 8.61)对照着看。
  • system vs process 偶尔倒挂属正常:本例 process 34.59% 略高于 system 31.78%,是因为两者由 不同 OS 计数器、各自独立采样 得出,瞬间不完全对齐 —— 差几个点是噪声。只有 持续、大幅 倒挂才要怀疑(多为容器 / cgroup 环境下两者基准不一致)。

排障口诀:process 高 + system 高 → 我的应用在吃 CPUtop -Hpjstack);system 高 + process 低 → CPU 被别的进程抢了process-1h 一路抬升 → 是持续恶化趋势,不是抖动

判断:CPU / 句柄健康、无锁争用;两点要管

好信号:CPU 不紧张、blocked ≈ 0(几乎没有锁竞争 / 死锁)、FD 充裕、线程平线无泄漏。 两点要留意:

  1. 日志量惊人:info 每分钟 300 多万行(峰值 640 万)。海量日志会 疯狂创建字符串对象,这极可能就是 5.5 里 Eden 被打满、5.7 里分配速率高达 ~1 GB/s 的 幕后推手。建议把生产日志级别从 INFO 收敛到 WARN,既减 I/O 又直接降 GC 压力;
  2. Load 峰值触及核数:平时富余,但高峰会短暂打满 8 核,结合 5.2 的昼夜流量节律,需确认峰值时段延迟是否达标。

5.5 堆内存池:Eden / Old / Survivor(全场最关键)

Grafana 4701 看板堆内存池区段:G1 Eden Space(高频锯齿)、G1 Old Gen(低位平直不涨)、G1 Survivor Space(周期方块)三张图并排

这一节是揭开 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 有没有泄漏

Grafana 4701 看板非堆内存池区段:Metaspace(水平线)、Compressed Class Space(低平线)与三个分段 CodeHeap 面板(No data)

先搞懂看板上的「Non-Heap」是什么

一句话:堆装你 new 出来的对象,非堆装 JVM 自己运转所需的基础设施 —— 主要是 Metaspace(类的元信息,即方法区)Code Cache(JIT 编译出的机器码)。它们一次加载好就长期不变,所以非堆曲线通常是 接近水平的直线,不像堆那样剧烈锯齿(详见 JVM 系列·入门篇)。

底层指标:jvm_classes_loaded_classesjvm_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 ×3JIT 机器码缓存(分段)No data查询标签不匹配

判断:Metaspace 健康;「No data」是看板的坑、不是故障

Metaspace 153 MiB 平直 → 没有反射 / 动态代理 / 热部署导致的类爆炸(若它 只增不减、不断爬坡,就要警惕,终点是元空间 OOM)。 那三个「No data」的分段 Code Cache 面板,是 看板 PromQL 与当前 JDK 的指标标签对不上(分段 Code Cache 指标名各 JDK 有差异),属于 2.2 提到的「命名不一致」坑 —— 修查询即可,与 JVM 本身无关

5.7 垃圾回收:G1 跑得怎么样

Grafana 4701 看板 GC 区段:Collections(GC 频率与类型)、Pause Durations(单次停顿时长)与 Allocated / Promoted(分配 / 晋升速率)三张图

底层指标:jvm_gc_pause_seconds(Timer,含 _count / _sum / _max,带 cause 标签)、jvm_gc_memory_allocated_bytes_totaljvm_gc_memory_promoted_bytes_total

面板含义本例实测解读
CollectionsGC 频率 / 类型~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 类加载:类的数量稳住了吗

Grafana 4701 看板类加载区段:Classes loaded(约 25K 条水平线)与 Class delta(每分钟加载 / 卸载类数,基本为 0)

面板含义本例实测解读
Classes loaded当前已加载类数25K 条水平线启动加载完就稳定,3 天没爬升
Class delta每分钟加载 / 卸载类数基本 0,仅启动初期有 +25 / -75之后再无 churn

判断:健康

loaded 这条线 持续往上爬,要警惕反射 / CGLIB / 热部署不停造类(呼应 5.6,终点是 Metaspace OOM)。这里启动后即平直、delta 归零,正常。

5.9 Buffer Pools:堆外内存有没有失控

Grafana 4701 看板 Buffer Pools 区段:Direct Buffers(used / count,平直不增长)、Utilisation(No data)与 Mapped Buffers(为 0)面板

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;

待办(按优先级):

  1. 海量日志:info 每分钟 300+ 万行 —— 这是 ~1 GB/s 分配率和 Eden 高频锯齿的主因,收敛日志级别可一举降低 I/O 与 GC 压力
  2. 极端长尾:5.2 那根冲到「小时级」的 Duration 尖刺要定位(与 GC / 慢任务时间点对齐);
  3. 内存预算偏紧:进程 ~10.9 GiB 占 16 GiB 整机近七成,留给 OS / page cache 余量不多,扩容或微调 -Xmx 前先评估;
  4. Load 峰值触及 8 核:高峰短暂跑满,确认峰值延迟达标;
  5. 看板修缮:CodeHeap / Process Memory / Utilisation 多处「No data」,是 PromQL 与本 JDK 标签不匹配,修查询即可。

六、常见现象的分析与处理

第五章是「正常看板长什么样」。这一章反过来:列出几类典型的「异常形态」,对应大概率原因和下一步动作,当成线上排障速查表用。每一行都能对应回第五章的某张面板。

看板上的现象大概率原因下一步动作
老年代基线台阶式上涨、GC 拉不回(看 5.5 Old Gen)内存泄漏jmap 导 heap dump,用 MAT 看支配树
Full GC 频繁、晋升速率高(看 5.7)大对象 / Survivor 太小 / 过早晋升-XmnSurvivorRatio,加 -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 频繁、线程 / 类只增不减。

从第五章那次实战拆解里,提炼出三条最该记住的「读图铁律」:

看板解读三条铁律

  1. 别被单个百分比吓到Heap used 76% 看着危险,拆到分代才发现是 Eden 的「虚高」、老年代很空。判断内存健康,永远看老年代趋势,而不是堆总量百分比
  2. 业务指标和 JVM 指标要对照着看:响应慢、长尾,先把 I/O 的时间点和 GC 停顿、堆锯齿、日志尖峰对齐 —— 把「业务现象」对到「JVM 内因」上。
  3. No data 多半是查询问题,不是故障:面板空白通常是 PromQL 与当前 JDK 的指标标签对不上(见 2.2),先怀疑看板、别怀疑 JVM。

原理 → 监控 → 诊断,三步连起来

JVM 深入》系列教你 JVM 内部怎么运作(知其所以然),这一篇教你 在线上怎么远远看见它的运作(出了事能预警、能缩小范围)。当看板把你领到某台机器门口、却答不上「具体是哪段代码」时,就该轮到 Arthas 这类在线诊断工具 登场 —— 不重启、不改代码地钻进去做现场探查。原理 → 监控 → 诊断,三者合起来才是从「会写 Java」到「扛得住线上」的完整闭环。

延伸阅读


views
Share this post on:

Previous Post
JMM 与可见性:volatile、happens-before 与重排序
Next Post
JDK 版本差异精讲:从 8 到 25,LTS 演进与关键能力