# 怎么分析 JVM 当前的内存占用情况?OOM 后怎么分析?
10 min read
Table of Contents
怎么分析 JVM 当前的内存占用情况?OOM 后怎么分析?
JVM 内存占用分析与 OOM 问题排查
【核心定义】
通过 JVM 提供的监控工具和命令行参数,实时观测堆内存、非堆内存及各内存区域的分配与使用情况,并在发生 OutOfMemoryError 时,结合内存快照和日志进行根因定位。
【关键要点】
- 实时监控工具:使用
jstat、jmap、jcmd等命令行工具,或 VisualVM、JConsole、Arthas 等图形化/命令行诊断工具,实时查看各内存池(Eden、Survivor、Old Gen、Metaspace)的使用量、峰值及 GC 频率。 - 内存溢出(OOM)类型识别:OOM 错误后附带的具体信息(如
Java heap space、Metaspace、Unable to create new native thread)直接指明了溢出区域,是首要分析线索。 - 内存快照(Heap Dump)分析:在 OOM 发生时或发生后立即触发 Heap Dump(
-XX:+HeapDumpOnOutOfMemoryError),使用 MAT、JProfiler 等工具分析对象占用、GC Roots 引用链,定位内存泄漏点。 - GC 日志分析:开启详细 GC 日志(
-Xlog:gc*或-XX:+PrintGCDetails),通过日志中的前后内存变化、Full GC 频率及效果,判断是内存泄漏还是单纯的内存不足。
【深度推导/细节】
内存占用分析流程拆解
Step 1:确定分析目标与工具选择
- 若需实时监控趋势,选用
jstat -gc <pid> 1000(每秒刷新)或 JConsole 可视化图表。 - 若需瞬间内存快照,使用
jmap -heap <pid>查看堆配置与使用概览,或jmap -histo <pid>查看对象实例数排名。 - 生产环境推荐使用 Arthas 的
dashboard、heapdump命令,无需预装,动态附着诊断。
Step 2:关键指标解读与健康度判断
- 堆内存:关注
Old Gen使用率是否持续上升且 Full GC 后回收效果差(内存泄漏迹象)。Eden区增长率结合Young GC频率可判断对象创建速率。 - 非堆内存:
Metaspace或CCS使用率若持续增长,可能存在类加载器泄漏或大量动态类生成(如 CGLib 代理)。 - 直接内存:通过
jcmd <pid> VM.native_memory查看Internal (malloc)部分,若持续增长且与堆内存增长不匹配,可能存在堆外内存泄漏(如 Netty 的DirectByteBuffer未释放)。
Step 3:结合 GC 日志进行动态分析
- 在日志中搜索
Full GC关键字,观察每次 Full GC 后老年代内存是否显著下降。若下降有限,则可能存在强引用持有的大对象或集合。 - 观察
Metaspace相关 GC 日志,若出现Metaspace的 Full GC 且回收量小,则可能存在类元数据泄漏。
OOM 后分析流程拆解
Step 1:立即保存现场
- 若已配置
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof,则自动生成堆转储文件。 - 若未配置,在 OOM 发生后尽快(避免进程重启)使用
jmap -dump:live,format=b,file=dump.hprof <pid>手动抓取快照(注意:live会触发 Full GC)。 - 同时保存对应的 GC 日志文件 和 应用日志文件(记录 OOM 发生时间点及上下文)。
Step 2:使用 MAT 进行泄漏嫌疑分析
- 打开 Heap Dump 文件,首先查看 Leak Suspects Report(泄漏嫌疑报告),MAT 会自动分析出占用内存最大的对象及可能的原因。
- 若报告不明确,使用 Histogram 功能,按
Shallow Heap或Retained Heap排序,找出数量异常或体积巨大的对象类(如char[]、String、自定义业务对象)。 - 对可疑类右键选择 Merge Shortest Paths to GC Roots -> exclude all phantom/weak/soft etc. references,仅保留强引用链,查看是哪个 GC Root 路径持有了这些对象,导致无法回收。
- 对于集合类(如
HashMap、ArrayList),可使用 Group Byoutgoing references功能,查看集合内部具体存放了哪些数据。
Step 3:交叉验证与根因定位
- 将 Heap Dump 中发现的疑似泄漏对象(如某个一直增长的
HashMap)与应用日志中的业务操作(如“用户查询”、“数据加载”)时间点进行关联,定位触发泄漏的代码路径。 - 检查代码中是否存在:
- 静态集合(如
static Map)长期添加而未清理。 - 缓存实现未设置大小限制或过期策略。
- 线程局部变量(
ThreadLocal)使用后未调用remove(),尤其在线程池场景下。 - 资源未关闭(如数据库连接、文件流、网络连接)。
- 静态集合(如
【关联/对比】
jstatvsjmap:jstat侧重于动态趋势监控(如 GC 时间、各分区容量变化),轻量且对进程影响小;jmap侧重于静态快照获取(如堆内存详情、生成 Dump 文件),某些操作(如dump)会暂停应用线程。- MAT vs JProfiler:MAT 是离线深度分析利器,尤其擅长内存泄漏的引用链分析,功能强大且免费;JProfiler 更侧重于实时在线 profiling,集成了 CPU、内存、线程等多维度监控,但属于商业软件。
- OOM vs StackOverflowError:OOM 发生在堆或元空间等数据区,通常由对象数量过多或过大引起;StackOverflowError 发生在虚拟机栈,由方法调用层次过深(如无限递归)导致,两者错误区域和根因完全不同。
『面试官追问』
-
除了 Heap Dump,还有哪些手段可以辅助分析内存问题?
- GC 日志分析工具:如 GCeasy、GCE Viewer,可自动化分析 GC 日志,生成吞吐量、暂停时间、内存提升率等报告。
- Native Memory Tracking (NMT):使用
-XX:NativeMemoryTracking=detail开启,通过jcmd <pid> VM.native_memory detail追踪 JVM 自身内存使用(堆、栈、代码区、GC等),用于诊断非堆的 native 内存泄漏。 - 操作系统级工具:在 Linux 下,使用
pmap -x <pid>查看进程内存映射,或top -Hp <pid>查看线程级 CPU 和内存消耗。
-
如何模拟和复现 OOM 问题?
- 堆内存 OOM:创建大对象(如
new byte[100 * 1024 * 1024])或通过循环向集合中添加对象,并保持强引用。 - Metaspace OOM:利用动态类生成技术(如 ASM、CGLib)在循环中不断创建新类。
- 直接内存 OOM:在循环中分配
ByteBuffer.allocateDirect()且不释放。 - 注意:模拟时需搭配相应的 JVM 参数限制内存大小(如
-Xmx10m)。
- 堆内存 OOM:创建大对象(如
-
线上环境为了预防 OOM,有哪些常用的 JVM 参数配置?
- 必备参数:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/logs:自动转储。-Xlog:gc*,gc+heap=debug:file=/path/to/gc.log:time,uptime,level,tags:filecount=5,filesize=100m:输出详细 GC 日志并滚动。-XX:ErrorFile=/path/to/hs_err_pid%p.log:保存 JVM 崩溃日志。
- 防护性参数:
- 限制元空间:
-XX:MaxMetaspaceSize=256m,防止无限制增长。 - 限制直接内存:
-XX:MaxDirectMemorySize=128m。 - 设置堆大小合理比例:
-XX:NewRatio=2(老年代/新生代=2/1),-XX:SurvivorRatio=8(Eden/Survivor=8/1)。
- 限制元空间:
- 必备参数:
-
如何区分是“内存泄漏”还是“内存溢出”?
- 内存泄漏(Memory Leak):指对象在逻辑上已不再使用,但由于错误的引用关系(通常是强引用)无法被 GC 回收,导致可用内存逐渐减少。其特点是:内存使用率随时间单调递增,即使触发 Full GC 也无法有效回收。
- 内存溢出(Memory Overflow):指应用确实需要这么多内存来存放存活对象,但分配的堆最大值(
-Xmx)不足以满足需求。其特点是:内存使用率在高水位波动,Full GC 后能回收大部分空间,但很快又会被填满。 - 判断方法:在压力测试或高峰时段,持续监控老年代使用率曲线。如果呈现“锯齿状”上升(每次 Full GC 后最低点都比前一次高),则是典型的内存泄漏。如果呈现“平台状”波动(每次 Full GC 后都能回到相近的低点),则更可能是容量不足的溢出。