一、项目背景
Java 服务发生 OOM(OutOfMemoryError)时,传统排查流程依赖人工操作:手动登录服务器、导出 .hprof 文件、本地安装 Eclipse MAT、加载分析,整个流程耗时长、依赖环境搭建,无法快速定位问题。
本项目实现了一套完全自动化的在线堆文件分析系统,支持一键触发、异步分析、结果回调,大幅缩短 OOM 排查链路。
二、系统架构
调用方 (jMetis)
│ POST /admin/heap/dumpAndAnalyze
▼
HprofController ──── enqueue ──▶ Redis SortedSet(任务队列)
│
HeapAnalyzeQueueWorker(每3s轮询)
│ setNX 双重锁
▼
HeapAnalyzeService.doAnalyze()
┌──────────┼──────────┐
Captain 飞书云盘 携程Ceph
└──────────┼──────────┘
│ NingParallelDownloader(分片并行)
▼
解压 (.gz / .zip / .tar.gz)
│
MatHprofAIParser.parse()
(绕过MAT OSGi,纯JVM内分析)
│ 7个分析维度
▼
结果写入Redis缓存(1天)
│
HTTP Callback → jMetis 平台
三、核心亮点
3.1 三种堆文件下载方式,覆盖全场景
系统定义了统一的下载接口 IDownloadHeapDump,通过策略模式注入,支持三种来源:
| 下载方式 | 枚举值 | 说明 |
|---|---|---|
| Captain 平台(自动触发) | CAPTAIN (0) |
调用 Captain API → 触发 jmap dump → 轮询任务状态 → 获取下载链接 → 下载 |
| 飞书云盘(用户上传后分析) | FEI_SHU (1) |
通过飞书开放平台 OAuth 获取 Token → 解析 fileToken → 并行下载 |
| 携程 Ceph 网盘 | CEPH (2) |
直接使用网盘下载链接并行下载 |
Captain 方式 为全自动模式:系统自动触发 dump(每日限制 3 次防止影响线上),轮询等待生成完成(最长 10 分钟),下载 URL 缓存 5 天避免重复触发。飞书和 Ceph 方式适合用户已手动导出堆文件后,提供链接进行在线分析。
3.2 突破 MAT OSGi 依赖,实现纯 JVM 内分析
Eclipse MAT 是业界标准的 Java 堆分析工具,但其设计依赖 Eclipse Platform 的 OSGi 容器(插件机制),无法直接嵌入 Spring Boot 服务内运行。
本项目通过反射 + 动态代理技术,完整绕过了 OSGi 依赖链,核心手段如下:
| 绕过点 | 技术手段 |
|---|---|
IExtensionRegistry(OSGi 插件注册表) |
JDK 动态代理注入 Mock Registry,返回空扩展列表 |
PlatformActivator.context(BundleContext) |
反射注入 Mock BundleContext,所有方法返回空/false |
MATPlugin.tracker(插件追踪器) |
反射注入 Noop IExtensionTracker |
PreliminaryIndexImpl(包私有构造器) |
反射访问包私有构造函数直接实例化 |
GarbageCleaner.clean()(包私有方法) |
反射调用包私有静态方法 |
完整解析管道(HprofParseUtil.openSnapshotDirect):
HprofIndexBuilder.fill() // 解析 HPROF,写磁盘索引
→ GarbageCleaner.clean() // 清理不可达对象
→ SnapshotImpl.create() // 直接构建 ISnapshot
这样 MAT 的完整分析能力(对象直方图、GC Root 链、支配树等)可以在无 Eclipse 环境的生产服务器上直接使用。
3.3 分片并行下载,解决大文件传输瓶颈
堆文件通常较大(数 GB),NingParallelDownloader 实现了基于 HTTP Range 请求的分片并行下载:
- HEAD 请求探测服务端是否支持 Range 及文件总大小
- 最多 5 个并发分片,每片最小 50 MB,基于文件大小自动计算分片数
- 使用 Ning AsyncHttpClient 实现非阻塞异步 IO,每个分片流式写盘,不占用 JVM 堆内存
- 分片失败自动重试最多 3 次(基于 Guava Retryer)
- 全部分片完成后使用 NIO FileChannel.transferTo 零拷贝合并,规避额外内存拷贝
- 服务端不支持 Range 时自动降级为单线程流式下载
3.4 异步队列 + 双重锁,保障多实例安全
请求接入后立即返回 "已受理,分析完成后回调",真正的分析由后台 Worker 异步执行:
Redis SortedSet(按入队时间排序)
↓ HeapAnalyzeQueueWorker 每3s轮询
AtomicBoolean(单机锁)------ 防同机器并发
↓ claimed
Redis setNX executingKey(多机锁)------ 防多实例竞争
↓ doAnalyze()
finally: ZREM + DEL(executingKey) + 释放本地锁
- 单机维度 :
AtomicBoolean.compareAndSet保证同一实例同一时刻只处理一个分析任务 - 多机维度:Redis setNX 竞争锁,抢占失败的实例本轮跳过,任务保留队列等待下次重试
- 故障恢复 :
executingKey设置 20 分钟 TTL,宕机后锁自动释放,任务可被其他实例接管
3.5 七维度全面分析,结果结构化输出
MatHprofAIParser 生成 MatHprofReportDraft,覆盖 7 个分析维度:
| Section | 内容 |
|---|---|
| S1 | 类直方图(按实例数排序) |
| S2 | 类直方图(按内存大小排序 Top N) |
| S3 | 支配树 Top 对象(保留堆最大的对象) |
| S4 | GC Root 引用链(泄漏路径追踪) |
| S5 | 线程快照全览 |
| S6 | 按类分组的 Top 持有者 |
| S7 | 大型集合对象(ArrayList / HashMap 等)检测 |
分析结果以结构化 JSON 缓存于 Redis(1 天有效期),相同 appId + podName 命中缓存直接返回,避免重复分析。
3.6 内存安全:防止 MAT 分析后内存泄漏
MAT 内部 BufferedRandomAccessInputStream 的 close() 只关闭了文件句柄,未清空 pages 字段(1.5M+ SoftReference<Page>),导致整条 SnapshotImpl → IndexManager → pages 引用链长期残留在老年代。
本项目在分析完成后通过反射主动置空 indexManager 和 heapObjectReader 字段,切断引用链,再调用 System.gc() 触发回收,确保每次分析后内存得到及时释放。同时注册 JVM ShutdownHook,保证服务停机时临时文件也被完整清理。
四、与传统方案对比
| 维度 | 传统方式 | 本项目 |
|---|---|---|
| 触发方式 | 手动登录服务器执行 jmap | 飞书机器人/API 一键触发 |
| 环境依赖 | 需要本地安装 Eclipse MAT | 无需任何额外环境 |
| 分析速度 | 人工操作,至少 30 分钟起 | 全自动,分析完成后主动回调 |
| 下载渠道 | 只能 SCP/SFTP 拉取 | 支持 Captain/飞书云盘/携程网盘 3 种方式 |
| 多实例安全 | 无保障 | 双重锁保证串行安全 |
| 结果可复用 | 每次重新分析 | Redis 缓存 1 天,相同文件秒返回 |
| 大文件传输 | 单线程,速度慢 | 分片并行,最高 5x 提速 |
五、总结
dumpAndAnalyze 功能将 Java OOM 排查从人工、离线、依赖本地工具 的传统模式,升级为自动化、在线、结构化输出的现代 AIOps 范式。核心技术突破点在于:通过反射+动态代理完整绕过 MAT 的 Eclipse OSGi 依赖,使业界标准的堆分析能力可以内嵌到任意 Spring Boot 服务中运行;同时配套多来源下载、分片并行传输、双重锁异步队列等工程化手段,保障了系统在生产环境下的稳定性与可扩展性。