java内存泄漏问题排查和JVM调优

182 阅读5分钟

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命令查看进程占用内存情况。

image.png 这里主要关注下RES 进程当前实际使用的、未被换出到交换空间(swap out)的物理内存。包含堆内存、非堆内存、堆外内存等。如果RES值持续上升,堆内存保持稳定,说明堆外内存在上升。

这个时候开始考虑使用jmap获取堆转存储。分析那些对象产生的泄漏。

常见的内存泄漏,

HashMap 、队列、线程池、websocket等资源没有正确的释放,另外一种是我们的程序调用了native方法,但没有释放资源。

发现泄漏后,最直接的办法生成jvm对象快照。生成堆转储文件,

内存分析工具

MAT(Memory Analyzer Tool) :最常用的堆分析工具,支持自动检测泄漏嫌疑人(Leak Suspects)

image.png 泄漏嫌疑直接能看到使用websocket相关操作导致的

关键分析步骤:
  1. 识别 “可疑大对象”

    • 在 MAT 中查看 “Histogram”(直方图),按 “Retained Heap”(对象被回收后可释放的内存)排序,找出占用内存前几名的类(如某业务 POJO、集合类)。
    • 关注 “Shallow Heap”(对象本身大小)小但 “Retained Heap” 大的对象(通常是集合,持有大量子对象引用)。
  2. 追溯引用链(支配树 / 引用树)

    • 对可疑对象右键选择 “Merge Shortest Paths to GC Roots”(排除弱引用 / 软引用等可被 GC 自动回收的引用类型),查看谁在 “强引用” 该对象。
    • 核心是找到 “根对象”(如静态变量、线程对象、类加载器),这些对象不会被 GC 回收,若其引用链中包含本应释放的对象,则导致泄漏。

    例:若发现大量User对象被HashMap持有,且HashMapUserCache类的静态变量,而UserCache未设置过期清理逻辑,则可定位为 “静态缓存未释放” 导致的泄漏。

第二种情况

查看JVM内存分布

jcmd <pid> VM.native_memory summary scale=MB

image.png

  • reserved:JVM 向操作系统申请的总虚拟地址空间(不一定都用了)。
  • committed:实际分配并使用的物理内存(RSS 的一部分)。
区域保留(reserved)提交(committed)说明
Java Heap16103424 KB6998528 KBJava 堆内存,最大堆和当前使用堆。mmap 分配。
Class1155163 KB117211 KB类元数据(Metaspace),包括类定义、方法区等。mmap 是 Metaspace,malloc 是其他类相关结构。
Thread1359766 KB1359766 KB每个线程的栈空间。你这里有 431 个线程,每个线程栈大约 3MB。
Code260713 KB66561 KBJIT 编译后的本地代码缓存。mmap 是代码缓存区,malloc 是编译器运行时分配。
GC616030 KB580490 KBGC 自身使用的内存,比如卡表、标记位图等。
Compiler509 KB509 KBJIT 编译器自身运行所需的内存。
Internal979145 KB979145 KBJVM 内部使用的内存,比如 DirectByteBuffer、JNI 分配等。
Symbol20415 KB20415 KB字符串表、常量池符号等。
Native Memory Tracking4420 KB4420 KBNMT 自身开销。
Arena Chunk231 KB231 KBJVM 内部 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"