Java语言本身支持垃圾自动回收,在日常编程中我们几乎没有关注过对象回收的问题。解决内存泄漏可能停留在八股文上。今天分享一次项目中内存泄漏的排查流程到最终解决。
java自带了强大的命令行工具
| 工具 | 核心功能 | 主要应用场景 | 关键命令/示例 |
|---|---|---|---|
**jcmd** | 多功能诊断:一个强大的“瑞士军刀”,可执行丰富的诊断命令,涵盖JVM状态查询、内存、线程、GC控制等。 | 综合诊断与运维:获取完整JVM信息(参数、属性)、动态控制(触发GC、开启JFR)、生成线程/堆转储。 | jcmd <pid> VM.flags(查看JVM参数) jcmd <pid> Thread.print(生成线程转储) jcmd <pid> GC.heap_dump(生成堆转储) |
**jmap** | 堆内存分析:专用于Java堆内存(Heap Memory)的直方图统计和堆转储(Heap Dump)文件生成。 | 深度内存分析:排查内存泄漏、分析堆内对象分布、生成堆转储文件供MAT等工具深度分析。 | jmap -histo:live <pid>(存活对象直方图) jmap -dump:live,format=b,file=heap.hprof <pid>(生成堆转储) |
**jstat** | JVM统计监控:持续监控JVM各种运行时指标,特别是垃圾回收(GC)相关数据和类加载信息。 | 实时性能监控:监控GC行为(频率、耗时)、各内存区使用率、类加载/卸载情况,用于性能调优。 | jstat -gcutil <pid> <interval> <count>(GC情况摘要) jstat -gc <pid> 1000 5(GC详情,每秒1次共5次) |
Java垃圾回收算法
Java垃圾回收算法,分代垃圾回收算法
- 新生代:复制算法
- 老年代:标记-清除算法和标记-整理算法
排查思路
在解决问题的时候,先了解下我们项目JVM参数信息.
jcmd <pid> VM.flags
-XX:CICompilerCount=15 -XX:InitialHeapSize=1031798784 -XX:MaxHeapSize=16489906176 -XX:MaxNewSize=5496635392 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=343932928 -XX:OldSize=687865856 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
第一种情况:
项目运行在linux服务器上,进行压测,内存以肉眼可见的速度上升。 使用top命令查看进程占用内存情况。
这里主要关注下RES
进程当前实际使用的、未被换出到交换空间(swap out)的物理内存。包含堆内存、非堆内存、堆外内存等。如果RES值持续上升,堆内存保持稳定,说明堆外内存在上升。
这个时候开始考虑使用jmap获取堆转存储。分析那些对象产生的泄漏。
常见的内存泄漏,
HashMap 、队列、线程池、websocket等资源没有正确的释放,另外一种是我们的程序调用了native方法,但没有释放资源。
发现泄漏后,最直接的办法生成jvm对象快照。生成堆转储文件,
内存分析工具
MAT(Memory Analyzer Tool) :最常用的堆分析工具,支持自动检测泄漏嫌疑人(Leak Suspects)
泄漏嫌疑直接能看到使用websocket相关操作导致的
关键分析步骤:
-
识别 “可疑大对象” :
- 在 MAT 中查看 “Histogram”(直方图),按 “Retained Heap”(对象被回收后可释放的内存)排序,找出占用内存前几名的类(如某业务 POJO、集合类)。
- 关注 “Shallow Heap”(对象本身大小)小但 “Retained Heap” 大的对象(通常是集合,持有大量子对象引用)。
-
追溯引用链(支配树 / 引用树) :
- 对可疑对象右键选择 “Merge Shortest Paths to GC Roots”(排除弱引用 / 软引用等可被 GC 自动回收的引用类型),查看谁在 “强引用” 该对象。
- 核心是找到 “根对象”(如静态变量、线程对象、类加载器),这些对象不会被 GC 回收,若其引用链中包含本应释放的对象,则导致泄漏。
例:若发现大量
User对象被HashMap持有,且HashMap是UserCache类的静态变量,而UserCache未设置过期清理逻辑,则可定位为 “静态缓存未释放” 导致的泄漏。
第二种情况
查看JVM内存分布
jcmd <pid> VM.native_memory summary scale=MB
- reserved:JVM 向操作系统申请的总虚拟地址空间(不一定都用了)。
- committed:实际分配并使用的物理内存(RSS 的一部分)。
| 区域 | 保留(reserved) | 提交(committed) | 说明 |
|---|---|---|---|
| Java Heap | 16103424 KB | 6998528 KB | Java 堆内存,最大堆和当前使用堆。mmap 分配。 |
| Class | 1155163 KB | 117211 KB | 类元数据(Metaspace),包括类定义、方法区等。mmap 是 Metaspace,malloc 是其他类相关结构。 |
| Thread | 1359766 KB | 1359766 KB | 每个线程的栈空间。你这里有 431 个线程,每个线程栈大约 3MB。 |
| Code | 260713 KB | 66561 KB | JIT 编译后的本地代码缓存。mmap 是代码缓存区,malloc 是编译器运行时分配。 |
| GC | 616030 KB | 580490 KB | GC 自身使用的内存,比如卡表、标记位图等。 |
| Compiler | 509 KB | 509 KB | JIT 编译器自身运行所需的内存。 |
| Internal | 979145 KB | 979145 KB | JVM 内部使用的内存,比如 DirectByteBuffer、JNI 分配等。 |
| Symbol | 20415 KB | 20415 KB | 字符串表、常量池符号等。 |
| Native Memory Tracking | 4420 KB | 4420 KB | NMT 自身开销。 |
| Arena Chunk | 231 KB | 231 KB | JVM 内部 arena 分配器使用的内存块。 |
**Internal** 这一项记录的是 JVM 用于其内部操作和数据结构所分配的原生内存。这部分内存不受堆内存大小(-Xmx)参数的直接限制,其使用量的异常增长常是堆外内存问题的关键线索。下表列出了 Internal部分主要包含的内存分配类型。
| 内存类别 | 说明 |
|---|---|
| 直接字节缓冲区 | 通过 ByteBuffer.allocateDirect()分配的堆外内存,是 Internal中最常见的部分。 |
| JNI 内存分配 | 在通过 Java Native Interface 调用本地代码时,本地代码内部通过 malloc等函数分配的内存。 |
| JVM 内部数据结构 | JVM 运行时维护的各种内部数据结构,例如用于性能监控、命令行解析、JVMTI 等操作的内存 |
查看internal部分的详细内容
jcmd <pid> VM.native_memory detail | grep -A 20 "Internal"