乐于分享
好东西不私藏

JVM 内存泄露排查:MAT 工具实战,揪出那个吃内存的"怪兽"

JVM 内存泄露排查:MAT 工具实战,揪出那个吃内存的"怪兽"

JVM 内存泄露排查:MAT 工具实战,揪出那个吃内存的”怪兽”

一、概述

1.1 背景介绍

在 Java 应用的生产环境中,内存泄露(Memory Leak)是最常见也最难排查的问题之一。随着应用运行时间增长,堆内存持续增加,最终导致频繁 Full GC 甚至 OOM(Out Of Memory)崩溃。传统的日志分析和监控难以定位具体的泄露对象,而 Eclipse Memory Analyzer(MAT)作为专业的堆转储分析工具,能够快速定位内存泄露的根本原因,帮助开发人员精准找到那些”占着茅坑不拉屎”的对象。

1.2 技术特点

  • • 强大的分析能力:支持超大堆转储文件(GB级别)的快速分析,自动识别内存泄露嫌疑点
  • • 可视化诊断:提供直观的对象引用链、支配树、直方图等多维度视图
  • • 智能报告:自动生成 Leak Suspects Report,快速定位问题根源
  • • 扩展性强:支持 OQL(Object Query Language)查询和插件扩展

1.3 适用场景

  • • 场景一:生产环境频繁出现 Full GC,应用响应缓慢甚至假死
  • • 场景二:监控显示堆内存持续增长,无法回收,最终触发 OOM
  • • 场景三:应用运行一段时间后性能下降明显,需要定期重启才能恢复
  • • 场景四:开发阶段的内存优化和泄露预防

1.4 环境要求

组件
版本要求
说明
操作系统
Windows/Linux/macOS
跨平台支持
JDK
JDK 8+
需与目标应用JDK版本匹配
MAT
1.13+
推荐最新稳定版
内存
至少为堆转储文件大小的1.5倍
分析10GB堆转储需要15GB+可用内存

二、详细步骤

2.1 准备工作

◆ 2.1.1 获取堆转储文件

方式一:主动触发(推荐生产环境使用)

# 方法1: 使用 jmap 生成堆转储jmap -dump:live,format=b,file=/tmp/heap_dump.hprof <pid># 方法2: 使用 jcmd (JDK 8+推荐)jcmd <pid> GC.heap_dump /tmp/heap_dump.hprof# 方法3: 使用 jvisualvm 图形化工具触发# 打开 jvisualvm -> 选择进程 -> Monitor -> Heap Dump

方式二:OOM时自动生成

# 在JVM启动参数中添加java -XX:+HeapDumpOnOutOfMemoryError \     -XX:HeapDumpPath=/var/log/java/heap_dump.hprof \     -jar your-application.jar

说明:

  • • live 参数会先触发一次 Full GC,过滤掉已经死亡的对象,推荐使用
  • • 堆转储过程会触发 STW(Stop The World),建议在低峰期操作
  • • 生成的 hprof 文件大小约等于堆内存使用量,需确保磁盘空间充足

◆ 2.1.2 安装 MAT 工具

# Linux/Mac 下载并解压wget https://www.eclipse.org/downloads/download.php?file=/mat/1.14.0/rcp/MemoryAnalyzer-1.14.0-linux.gtk.x86_64.zipunzip MemoryAnalyzer-1.14.0-linux.gtk.x86_64.zipcd mat./MemoryAnalyzer# Windows: 直接下载安装包安装# https://www.eclipse.org/mat/downloads.php

调整 MAT 内存配置

# 编辑 MemoryAnalyzer.ini-vmargs-Xmx16g# 设置为堆转储文件大小的1.5倍

2.2 核心分析流程

◆ 2.2.1 导入堆转储文件

# 启动 MAT 后File -> Open Heap Dump -> 选择 heap_dump.hprof# 选择分析报告类型(首次分析推荐)Leak Suspects Report

说明:MAT 会自动解析堆转储文件并生成索引,第一次打开较慢,后续会使用缓存加速。

◆ 2.2.2 Leak Suspects Report 分析

MAT 自动生成的泄露嫌疑报告会标注出最可能的内存泄露点:

报告解读要点:

  • • Problem Suspect 1/2/3:MAT 识别出的前3个最可疑的内存泄露点
  • • Shortest Paths To the Accumulation Point:从 GC Root 到泄露对象的最短引用链
  • • Accumulated Objects by Class:被大量累积的对象类型统计

示例分析:

Problem Suspect 1----------------The thread "http-nio-8080-exec-23" keeps local variables with total size 2,145,678,912 bytes.Suspects:- One instance of "java.util.HashMap" loaded by "<system class loader>"  occupies 2,045,123,456 (95.32%) bytes.Keywords:java.util.HashMapcom.example.cache.LocalCache

◆ 2.2.3 使用 Dominator Tree(支配树)

Toolbar -> Dominator Tree

操作步骤:

  1. 1. 按 Retained Heap 排序(默认),找到占用内存最多的对象
  2. 2. 右键目标对象 -> Path to GC Roots -> exclude weak/soft references
  3. 3. 分析引用链,找到持有该对象的根本原因

参数说明:

  • • Shallow Heap:对象自身占用内存大小
  • • Retained Heap:对象被回收后可释放的总内存(包含引用对象)
  • • Percentage:占总堆内存的百分比

◆ 2.2.4 Histogram(直方图)分析

Toolbar -> Histogram

查找可疑对象:

# 右键列表 -> Group by package# 快速定位业务代码中的对象# 查看某个类的所有实例右键 com.example.User -> List objects -> with incoming references

2.3 高级分析技巧

◆ 2.3.1 OQL 查询

-- 查找所有 size > 10000 的 ArrayListSELECT*FROM java.util.ArrayList WHERE size() >10000-- 查找特定包下的所有对象SELECT*FROM com.example.model.*WHERE (toString() LIKE ".*leaked.*")-- 查找持有大量对象的 MapSELECT s, s.size FROM java.util.HashMap s WHERE s.size >1000-- 查找特定字段值的对象SELECT*FROM com.example.User WHERE name.toString() = "admin"

◆ 2.3.2 线程分析

Toolbar -> Thread Overview

查看线程栈持有的对象:

右键线程 -> List objects-> with incoming references  # 查看被哪些对象引用-> with outgoing references  # 查看引用了哪些对象

◆ 2.3.3 对比分析

# 分析两个不同时间点的堆转储# 1. 打开第一个 hprof 文件# 2. 工具栏 -> Compare To Another Heap Dump# 3. 选择第二个 hprof 文件# 对比结果会显示:- 新增对象- 增长最快的对象- 可能的泄露点

三、示例代码和配置

3.1 完整配置示例

◆ 3.1.1 JVM 启动参数配置

# 生产环境推荐配置java -Xms4g -Xmx4g \     -XX:+HeapDumpOnOutOfMemoryError \     -XX:HeapDumpPath=/var/log/java/heap_dump_%p_%t.hprof \     -XX:+UseG1GC \     -XX:MaxGCPauseMillis=200 \     -XX:+PrintGCDetails \     -XX:+PrintGCDateStamps \     -Xloggc:/var/log/java/gc_%p.log \     -XX:+UseGCLogFileRotation \     -XX:NumberOfGCLogFiles=5 \     -XX:GCLogFileSize=20M \     -jar your-application.jar

◆ 3.1.2 内存泄露监控脚本

#!/bin/bash# 文件名: monitor_memory_leak.sh# 功能: 定期检测JVM内存使用,超过阈值自动生成堆转储APP_NAME="your-application"HEAP_THRESHOLD=85  # 堆内存使用超过85%触发DUMP_DIR="/var/log/heap_dumps"LOG_FILE="/var/log/memory_monitor.log"mkdir -p "$DUMP_DIR"whiletruedo# 获取Java进程ID    PID=$(pgrep -f "$APP_NAME")if [ -z "$PID" ]; thenecho"[$(date)] 应用未运行" >> "$LOG_FILE"sleep 60continuefi# 获取堆内存使用率    HEAP_USAGE=$(jstat -gcutil "$PID" | tail -1 | awk '{print $4}')echo"[$(date)] PID: $PID, 老年代使用率: ${HEAP_USAGE}%" >> "$LOG_FILE"# 判断是否超过阈值if (( $(echo "$HEAP_USAGE > $HEAP_THRESHOLD" | bc -l) )); then        TIMESTAMP=$(date +%Y%m%d_%H%M%S)        DUMP_FILE="${DUMP_DIR}/heap_dump_${PID}_${TIMESTAMP}.hprof"echo"[$(date)] 触发堆转储: $DUMP_FILE" >> "$LOG_FILE"# 生成堆转储        jcmd "$PID" GC.heap_dump "$DUMP_FILE"# 发送告警(集成你的告警系统)# curl -X POST "http://alert.example.com/api/alert" \#      -d "message=堆内存使用率超过${HEAP_THRESHOLD}%,已生成堆转储"# 生成后等待30分钟再检测sleep 1800fisleep 300  # 每5分钟检测一次done

3.2 实际应用案例

◆ 案例一:ThreadLocal 导致的内存泄露

场景描述:Web 应用使用 ThreadLocal 存储用户上下文,但在请求结束后未正确清理,导致 Tomcat 线程池中的线程持续持有大量用户数据。

问题代码:

publicclassUserContextHolder {privatestaticfinal ThreadLocal<UserContext> context = newThreadLocal<>();publicstaticvoidsetContext(UserContext user) {        context.set(user);  // 设置用户上下文    }publicstatic UserContext getContext() {return context.get();    }// 问题:缺少 remove() 方法调用}// Controller 中使用@RestControllerpublicclassUserController {@GetMapping("/api/user")public User getUser() {UserContextctx=newUserContext();        ctx.setUserData(loadLargeUserData());  // 加载大量数据        UserContextHolder.setContext(ctx);// 业务逻辑...// 问题:请求结束后未清理 ThreadLocalreturn user;    }}

MAT 分析步骤:

  1. 1. 打开堆转储文件 -> 查看 Leak Suspects Report
  2. 2. 发现问题:Tomcat 工作线程持有大量 ThreadLocalMap 对象
  3. 3. Dominator Tree -> 找到 Thread 对象 -> 展开 threadLocals
  4. 4. Path to GC Roots -> 发现引用链:

    Thread "http-nio-8080-exec-1"-> threadLocals (ThreadLocalMap)-> Entry[]-> Entry.value (UserContext)-> userData (byte[1048576])  // 1MB 用户数据

修复方案:

publicclassUserContextHolder {privatestaticfinal ThreadLocal<UserContext> context = newThreadLocal<>();publicstaticvoidsetContext(UserContext user) {        context.set(user);    }publicstatic UserContext getContext() {return context.get();    }// 添加清理方法publicstaticvoidclear() {        context.remove();    }}// 使用 Filter 或拦截器统一清理@ComponentpublicclassUserContextFilterimplementsFilter {@OverridepublicvoiddoFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {try {            chain.doFilter(request, response);        } finally {// 请求结束后清理 ThreadLocal            UserContextHolder.clear();        }    }}

◆ 案例二:静态集合缓存导致的泄露

场景描述:为了提升性能,使用静态 HashMap 缓存查询结果,但缺少过期机制,导致缓存无限增长。

问题代码:

publicclassUserCache {// 问题:静态缓存,永远不会被GCprivatestaticfinal Map<Long, User> USER_CACHE = newHashMap<>();public User getUser(Long id) {if (!USER_CACHE.containsKey(id)) {Useruser= userRepository.findById(id);            USER_CACHE.put(id, user);  // 持续添加,永不删除        }return USER_CACHE.get(id);    }}

MAT 分析结果:

Histogram 视图:Class Name                          | Objects | Shallow Heap | Retained Heap------------------------------------|---------|--------------|---------------java.util.HashMap$Node              | 1,234,567 | 59 MB      | 2.3 GBcom.example.model.User              | 1,234,567 | 48 bytes   | 1.8 GBcom.example.cache.UserCache         | 1       | 24 bytes   | 2.3 GB

修复方案:

import com.github.benmanes.caffeine.cache.Cache;import com.github.benmanes.caffeine.cache.Caffeine;publicclassUserCache {// 使用带过期机制的缓存privatestaticfinal Cache<Long, User> USER_CACHE = Caffeine.newBuilder()            .maximumSize(10_000)  // 最大缓存数量            .expireAfterWrite(Duration.ofHours(1))  // 1小时后过期            .expireAfterAccess(Duration.ofMinutes(30))  // 30分钟未访问则过期            .build();public User getUser(Long id) {return USER_CACHE.get(id, k -> userRepository.findById(k));    }}

◆ 案例三:事件监听器未注销

场景描述:动态注册事件监听器,但在对象销毁时未注销,导致监听器和相关对象无法被回收。

问题代码:

publicclassDataProcessor {privatefinal List<byte[]> dataBuffer = newArrayList<>();publicDataProcessor(EventBus eventBus) {// 注册监听器        eventBus.register(this);  // 问题:EventBus 持有 this 引用    }@SubscribepublicvoidhandleEvent(DataEvent event) {        dataBuffer.add(event.getData());  // 持续累积数据    }// 问题:对象销毁时未调用 eventBus.unregister(this)}// 使用场景for (inti=0; i < 1000; i++) {DataProcessorprocessor=newDataProcessor(eventBus);// processor 使用完毕,期望被GC回收// 但由于 EventBus 持有引用,无法回收}

修复方案:

publicclassDataProcessorimplementsAutoCloseable {privatefinal EventBus eventBus;privatefinal List<byte[]> dataBuffer = newArrayList<>();publicDataProcessor(EventBus eventBus) {this.eventBus = eventBus;        eventBus.register(this);    }@SubscribepublicvoidhandleEvent(DataEvent event) {        dataBuffer.add(event.getData());    }@Overridepublicvoidclose() {// 注销监听器,释放引用        eventBus.unregister(this);        dataBuffer.clear();    }}// 使用 try-with-resources 自动清理try (DataProcessorprocessor=newDataProcessor(eventBus)) {// 使用 processor}  // 自动调用 close()

四、最佳实践和注意事项

4.1 最佳实践

◆ 4.1.1 性能优化

  • • 优化点一:减少堆转储文件大小

    # 使用 live 参数,只转储存活对象jmap -dump:live,format=b,file=heap.hprof <pid># 或者先手动触发Full GCjcmd <pid> GC.runjcmd <pid> GC.heap_dump heap.hprof
  • • 优化点二:增量分析
    • • 先生成基准堆转储(T0)
    • • 运行一段时间后生成第二个堆转储(T1)
    • • 使用 MAT 的 Compare 功能对比差异
    • • 重点关注新增和增长的对象
  • • 优化点三:分段分析大文件

    # 对于超大堆转储(>50GB),可以使用命令行模式./ParseHeapDump.sh heap.hprof org.eclipse.mat.api:suspects# 或者使用 jhat 进行初步筛选jhat -J-Xmx16g heap.hprof# 访问 http://localhost:7000 查看初步分析

◆ 4.1.2 安全加固

  • • 安全措施一:保护敏感信息

    # 堆转储可能包含密码等敏感信息# 生成后立即加密openssl enc -aes-256-cbc -salt -in heap.hprof -out heap.hprof.enc# 分析时解密openssl enc -d -aes-256-cbc -in heap.hprof.enc -out heap.hprof
  • • 安全措施二:限制堆转储文件访问权限

    # 设置严格的文件权限chmod 600 /var/log/java/*.hprofchown app:app /var/log/java/*.hprof
  • • 安全措施三:定期清理历史堆转储

    # 只保留最近7天的堆转储文件find /var/log/java -name "*.hprof" -mtime +7 -delete

◆ 4.1.3 预防性监控

  • • 监控指标:
    • • 老年代(Old Gen)使用率持续 > 80%
    • • Full GC 频率 > 1次/分钟
    • • Full GC 后堆内存回收率 < 30%
    • • 应用响应时间 P99 持续上涨
  • • 监控脚本:
#!/bin/bash# 实时监控GC情况jstat -gcutil <pid> 1000 10 | awk 'NR>1 {    if ($4 > 80) {        print "警告: 老年代使用率 " $4 "%"    }    if ($6 > 10) {        print "警告: Full GC次数 " $6    }}'

4.2 注意事项

◆ 4.2.1 配置注意事项

⚠️ 警告:生成堆转储会触发 STW,生产环境需谨慎操作

  • • ❗ 注意事项一:堆转储期间应用完全停止响应,大堆(>10GB)可能持续数分钟,建议:
    • • 在负载均衡中摘除节点后再操作
    • • 选择业务低峰期执行
    • • 使用 jcmd 代替 jmap,性能更好
  • • ❗ 注意事项二:确保磁盘空间充足,至少为堆内存大小的 1.5 倍

    # 操作前检查磁盘空间df -h /var/log/java
  • • ❗ 注意事项三:MAT 分析需要大量内存,确保分析机器有足够资源

    # MemoryAnalyzer.ini 配置-Xmx32g  # 设置为堆转储大小的1.5-2倍

◆ 4.2.2 常见错误

错误现象
原因分析
解决方案
生成堆转储时 OOM
堆内存不足
增加 -Xmx 或使用 jmap -F 强制转储
MAT 打开文件时卡死
MAT 内存配置过小
增大 MemoryAnalyzer.ini 中的 -Xmx
堆转储文件损坏
生成过程中应用崩溃
使用 jhat -J-Xmx8g heap.hprof 验证
找不到 GC Root
使用了 dump:live 参数
去掉 live 参数重新生成
分析结果与实际不符
堆转储时机不对
在问题复现时立即生成
线程栈显示不完整
JVM 参数缺少调试信息
添加 -XX:+UnlockDiagnosticVMOptions

◆ 4.2.3 兼容性问题

  • • 版本兼容:MAT 版本需与 JDK 版本匹配
    • • JDK 8: MAT 1.10+
    • • JDK 11: MAT 1.11+
    • • JDK 17: MAT 1.13+
  • • 平台兼容:不同 JVM 的堆转储格式略有差异
    • • HotSpot VM: 完全支持
    • • OpenJ9: 需要使用 PHD 格式
    • • GraalVM: 需要使用 –vm.XX:+HeapDumpOnOutOfMemoryError
  • • 格式兼容:

    # 确认堆转储格式file heap.hprof# 输出: heap.hprof: Java hprof dump, binary format

五、故障排查和监控

5.1 故障排查

◆ 5.1.1 日志查看

# 查看 GC 日志,分析内存回收情况tail -f /var/log/java/gc.log | grep "Full GC"# 分析 GC 日志统计cat gc.log | grep "Full GC" | awk '{print $1, $NF}' | tail -20# 使用 GCeasy 在线分析工具# 上传 gc.log 到 https://gceasy.io/

◆ 5.1.2 常见问题排查

问题一:Full GC 频繁但内存回收正常

# 诊断命令jstat -gcutil <pid> 1000 10# 观察 OU (Old Generation Utilization)# 如果 Full GC 后 OU 下降明显,说明不是内存泄露,可能是:# 1. 堆内存设置过小# 2. 对象提升速度过快

解决方案:

  1. 1. 增大堆内存

    -Xms8g -Xmx8g  # 原来是 4g
  2. 2. 调整新生代比例

    -XX:NewRatio=2  # 新生代:老年代 = 1:2
  3. 3. 调整对象晋升阈值

    -XX:MaxTenuringThreshold=15  # 默认15,可适当提高

问题二:Full GC 后内存无法回收

# 诊断命令jstat -gccause <pid> 1000# 如果 Full GC 后 OU 持续 > 80%,确认内存泄露# 立即生成堆转储jcmd <pid> GC.heap_dump /tmp/leak_$(date +%Y%m%d_%H%M%S).hprof

解决方案:按照本文 MAT 分析流程定位泄露点

问题三:应用启动后立即 OOM

  • • 症状:应用刚启动就报 OutOfMemoryError
  • • 排查:

    # 查看启动时加载的类数量jmap -histo <pid> | head -20# 检查是否有超大对象jmap -heap <pid>
  • • 解决:
    • • 检查是否一次性加载了大量数据(如预热缓存)
    • • 分批加载或使用懒加载
    • • 增大堆内存或减少初始化数据量

◆ 5.1.3 调试模式

# 开启详细 GC 日志java -XX:+PrintGCDetails \     -XX:+PrintGCDateStamps \     -XX:+PrintGCApplicationStoppedTime \     -XX:+PrintTenuringDistribution \     -XX:+PrintHeapAtGC \     -Xloggc:gc.log \     -jar app.jar# 使用 JFR (Java Flight Recorder) 进行持续监控java -XX:StartFlightRecording=duration=60s,filename=recording.jfr \     -jar app.jar# 分析 JFR 记录jfr print --events jdk.ObjectAllocationSample recording.jfr

5.2 性能监控

◆ 5.2.1 关键指标监控

# 实时监控脚本#!/bin/bashPID=$(pgrep -f your-app)whiletruedoecho"===== $(date) ====="# GC 统计    jstat -gcutil $PID 1000 1# 堆内存使用    jcmd $PID VM.native_memory summary | grep "Java Heap"# 线程数    jstack $PID | grep "java.lang.Thread.State" | wc -l# 加载的类数量    jstat -class $PID | tail -1sleep 60done

◆ 5.2.2 监控指标说明

指标名称
正常范围
告警阈值
说明
老年代使用率
< 70%
> 85%
持续高位需关注
Full GC 频率
< 1次/小时
> 1次/分钟
频繁 Full GC 影响性能
GC 暂停时间
< 200ms
> 1s
长时间暂停影响用户体验
堆内存增长率
稳定
持续增长
斜率持续上升为泄露征兆
存活对象大小
稳定
持续增长
Full GC 后仍增长说明有泄露

◆ 5.2.3 Prometheus 监控配置

# JMX Exporter 配置示例# jmx_exporter_config.ymllowercaseOutputName:truelowercaseOutputLabelNames:truerules:# Heap Memory-pattern:'java.lang<type=Memory><HeapMemoryUsage>(\w+)'name:jvm_heap_memory_$1type:GAUGE# GC Count-pattern:'java.lang<type=GarbageCollector, name=(\w+)><>CollectionCount'name:jvm_gc_collection_countlabels:gc:"$1"type:COUNTER# GC Time-pattern:'java.lang<type=GarbageCollector, name=(\w+)><>CollectionTime'name:jvm_gc_collection_time_mslabels:gc:"$1"type:COUNTER# Grafana 告警规则alert:HighHeapMemoryUsageexpr:(jvm_heap_memory_used/jvm_heap_memory_max)>0.85for:5mlabels:severity:warningannotations:summary:"堆内存使用率过高"description:"{{ $labels.instance }} 堆内存使用率 {{ $value }}%"alert:FrequentFullGCexpr:rate(jvm_gc_collection_count{gc="PSMarkSweep"}[5m])>0.2for:5mlabels:severity:criticalannotations:summary:"Full GC 频繁"description:"{{ $labels.instance }} 5分钟内 Full GC {{ $value }} 次"

5.3 备份与恢复

◆ 5.3.1 自动备份脚本

#!/bin/bash# 定期备份堆转储文件SOURCE_DIR="/var/log/java"BACKUP_DIR="/backup/heap_dumps"RETENTION_DAYS=30# 创建备份目录mkdir -p "$BACKUP_DIR"# 压缩并备份最新的堆转储find "$SOURCE_DIR" -name "*.hprof" -mtime -1 | whileread hprof_file; do    filename=$(basename"$hprof_file")    tar -czf "$BACKUP_DIR/${filename}.tar.gz""$hprof_file"echo"[$(date)] 已备份: $filename"done# 清理过期备份find "$BACKUP_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete# 上传到对象存储(可选)# aws s3 sync "$BACKUP_DIR" s3://your-bucket/heap-dumps/

◆ 5.3.2 问题重现环境准备

  1. 1. 保存现场:

    # 生成堆转储jcmd $PID GC.heap_dump heap.hprof# 保存线程栈jstack $PID > thread_dump.txt# 保存 JVM 参数jcmd $PID VM.flags > jvm_flags.txt# 打包环境信息tar -czf diagnostic_$(date +%Y%m%d_%H%M%S).tar.gz \    heap.hprof thread_dump.txt jvm_flags.txt gc.log
  2. 2. 本地分析:

    # 解压诊断包tar -xzf diagnostic_20250126_143022.tar.gz# 使用 MAT 分析mat heap.hprof
  3. 3. 问题复现:

    # 使用相同的 JVM 参数启动java $(cat jvm_flags.txt) -jar app.jar

六、总结

6.1 技术要点回顾

  • • ✅ 要点一:MAT 是分析 JVM 内存泄露的最强工具,通过 Leak Suspects Report、Dominator Tree、Histogram 等视图快速定位问题
  • • ✅ 要点二:常见内存泄露场景包括 ThreadLocal 未清理、静态集合缓存、事件监听器未注销、数据库连接未关闭等
  • • ✅ 要点三:生产环境应配置 -XX:+HeapDumpOnOutOfMemoryError 自动生成堆转储,并建立完善的监控告警机制
  • • ✅ 要点四:结合 GC 日志、JFR、监控指标等多维度分析,准确判断是否存在内存泄露

6.2 进阶学习方向

  1. 1. JVM 调优深入:
    • • 学习资源:Oracle JVM Tuning Guide
    • • 实践建议:在测试环境尝试不同 GC 算法(G1、ZGC、Shenandoah),对比效果
  2. 2. 分布式系统内存问题排查:
    • • 学习资源:Kubernetes Memory Management
    • • 实践建议:使用 Arthas 进行在线诊断,无需重启应用
  3. 3. Native Memory Leak 排查:
    • • 学习资源:Native Memory Tracking
    • • 实践建议:使用 -XX:NativeMemoryTracking=detail 追踪堆外内存

6.3 参考资料

  • • Eclipse MAT 官方文档 – 完整功能说明和示例
  • • 美团技术团队:Java内存泄漏分析系列 – 实战案例丰富
  • • Alibaba Arthas – 在线诊断神器
  • • GCeasy 在线分析 – GC 日志可视化分析

附录

A. 命令速查表

# 生成堆转储jmap -dump:live,format=b,file=heap.hprof <pid>  # jmap方式jcmd <pid> GC.heap_dump heap.hprof              # jcmd方式(推荐)# GC 统计jstat -gcutil <pid> 1000 10                     # 每秒输出GC统计,共10次jstat -gccause <pid>                            # 查看GC原因# 线程分析jstack <pid> > threads.txt                      # 导出线程栈jstack <pid> | grep "java.lang.Thread.State" | wc -l  # 统计线程数# 堆信息jcmd <pid> VM.heap_summary                      # 查看堆摘要jmap -heap <pid>                                # 查看堆详细配置# 类统计jmap -histo:live <pid> | head -30              # 查看存活对象统计# Native Memoryjcmd <pid> VM.native_memory summary            # 查看本地内存使用

B. MAT 快捷键

快捷键
功能
Ctrl + Q
打开 OQL 查询窗口
Ctrl + F
在当前视图中搜索
Ctrl + H
打开 Histogram 视图
Ctrl + D
打开 Dominator Tree
Ctrl + Shift+L
查看 Leak Suspects
F5
刷新当前视图
Ctrl + =
展开所有子节点

C. 术语表

术语
英文
解释
浅堆
Shallow Heap
对象自身占用的内存大小,不包括引用对象
保留堆
Retained Heap
对象被回收后可释放的总内存,包括其持有的所有引用对象
支配树
Dominator Tree
显示对象之间的支配关系,用于找到内存占用的根本原因
GC Root
Garbage Collection Root
GC 的起点,包括栈变量、静态变量、JNI 引用等
内存泄露
Memory Leak
不再使用的对象仍被引用,无法被 GC 回收
OOM
Out Of Memory
堆内存不足,无法分配新对象
对象提升
Object Promotion
对象从新生代晋升到老年代
STW
Stop The World
GC 时应用完全暂停的状态
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » JVM 内存泄露排查:MAT 工具实战,揪出那个吃内存的"怪兽"

猜你喜欢

  • 暂无文章