深入浅出 jmap:Java 内存分析的“显微镜“

文章目录

    • [什么是 jmap?](#什么是 jmap?)
    • 常用核心命令详解
      • [A. 对象统计:哪些类在"吃"内存?](#A. 对象统计:哪些类在“吃”内存?)
      • [B. 分区概览:老年代满了还是新生代不够?](#B. 分区概览:老年代满了还是新生代不够?)
      • [C. 堆转储:](#C. 堆转储:)
    • [真实案例分析:为何我的 RocksDB Flush 要 3 秒?](#真实案例分析:为何我的 RocksDB Flush 要 3 秒?)
    • [最后:使用 jmap 的注意事项](#最后:使用 jmap 的注意事项)

在处理大规模数据同步或高并发应用时,我们经常会遇到 Full GC 频繁、老年代(Old Gen)堆积或 吞吐量骤降的问题。要穿透这些现象看到本质,JDK 自带的命令行工具 jmap 是最直接的武器。

什么是 jmap?

jmap (Java Memory Map) 用于生成 Java 进程的内存映射。它可以让你看到堆内存的分区比例、查看内存中的对象统计,甚至可以将整个堆转储(Dump)下来进行离线分析。


常用核心命令详解

A. 对象统计:哪些类在"吃"内存?

当你发现内存占用过高时,第一步通常是查看"谁占得最多"。

bash 复制代码
jmap -histo <pid> | head -n 20
  • 作用:按占用空间大小降序列出内存中的类。

  • 实战意义 :如果你在结果中看到数亿个 java.util.HashMap$Node 或业务对象,说明你可能存在内存泄漏,或者缓存逻辑设置的阈值过大。

例如Java 进程号为1920,可以查看实例个数,总占用字节大小

bash 复制代码
ubuntu@aa:~/disk1$ jmap -histo 1920|head -n 30
 num     #instances         #bytes  class name (module)
-------------------------------------------------------
   1:     672670001    32288160048  io.pixelsdb.pixels.index.IndexProto$RowLocation
   2:     672675490    26907019600  java.util.HashMap$Node (java.base@23.0.2)
   3:     739751306    17754031344  java.lang.Long (java.base@23.0.2)
   4:      77497452    13548993440  [Ljdk.internal.vm.FillerElement; (java.base@23.0.2)
   5:          7163    10333864688  [Ljava.util.HashMap$Node; (java.base@23.0.2)
   6:      65840292     3687056352  java.util.LinkedHashMap$Entry (java.base@23.0.2)
   7:        737649      106948464  [B (java.base@23.0.2)
   8:        620951       54643688  io.pixelsdb.pixels.index.IndexProto$PrimaryIndexEntry$Builder
   9:       1241965       49678600  com.google.protobuf.SingleFieldBuilderV3
  10:        620951       49676080  io.pixelsdb.pixels.index.IndexProto$IndexKey$Builder

小贴士 :使用 jmap -histo:live <pid> 可以只统计活对象。这会强制触发一次 Full GC,虽然会造成应用卡顿,但能帮你确认哪些对象是真正无法回收的。

B. 分区概览:老年代满了还是新生代不够?

了解堆内存的逻辑分区(Eden, Survivor, Old Gen)的利用率。

bash 复制代码
jmap -heap <pid>

# or

sudo jhsdb jmap --pid <PID> --heap
  • 关键指标

    • Old Generation :如果 used 接近 100%,说明老年代已满,这是导致程序变慢、系统卡顿(STW)的根本原因。
    • MaxHeapSize:确认 JVM 实际分配的最大堆内存是否符合预期。

示例:

bash 复制代码
ubuntu@realtime-pixels-retina:~/disk1$ sudo jhsdb jmap --pid 2921 --heap 
Attaching to process ID 2921, please wait...                                                                                                                            
Debugger attached successfully.                                                   
Server compiler detected.                                                              
JVM version is 23.0.2+7                                                                                                                                       
using thread-local object allocation.                                                   
Garbage-First (G1) GC with 13 thread(s)                                                                
Heap Configuration:                                                            
   MinHeapFreeRatio         = 40                                               
   MaxHeapFreeRatio         = 70                                               
   MaxHeapSize              = 118111600640 (112640.0MB)
   NewSize                  = 1363144 (1.0MB)
   MaxNewSize               = 70866960384 (67584.0MB)
   OldSize                  = 5452592 (5.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 22020096 (21.0MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 18446744073709551615 (1.7592186044415E13MB)
   G1HeapRegionSize       = 33554432 (32.0MB)

Heap Usage:

G1 Heap:
   regions  = 3520
   capacity = 118111600640 (112640.0MB)
   used     = 1728055920 (1648.0MB)
   free     = 116383544720 (110991.0MB)
   1.4630704440853812% used
G1 Young Generation:
Eden Space:
   regions  = 47
   capacity = 1711276032 (1632.0MB)
   used     = 1577058304 (1504.0MB)
   free     = 134217728 (128.0MB)
   92.15686274509804% used
Survivor Space:
   regions  = 0
   capacity = 33554432 (32.0MB)
   used     = 9478560 (9.0MB)
   free     = 24075872 (22.0MB)
   28.248310089111328% used
G1 Old Generation:
   regions  = 5
   capacity = 2550136832 (2432.0MB)
   used     = 141519056 (134.0MB)
   free     = 2408617776 (2297.0MB)
   5.549469119624088% used

C. 堆转储:

当对象关系过于复杂(比如几层嵌套的Map),仅看统计表无法定位是谁持有引用时,需要导出 Dump 文件。

bash 复制代码
jmap -dump:format=b,file=heap_dump.hprof <pid>
  • 后续步骤 :将生成的 .hprof 文件下载到本地,使用 Eclipse MAT (Memory Analyzer Tool)JProfiler 打开。通过 Path to GC Roots 功能,你可以直接抓到那个没被关闭的静态集合或长生命周期对象。

真实案例分析:为何我的 RocksDB Flush 要 3 秒?

在最近的一次实验中,通过 jmap -histo 发现内存中存在 11.2 亿个 HashMap$Node ,占用空间高达 45GB

  1. 现象:RocksDB 的Flush P50 延迟高达 3 秒。
  2. 通过 jmap 发现:老年代几乎被这些 Node 对象填满。
  3. 推论:由于老年代对象过多,JVM 在进行 G1 扫描(Scan Roots)时需要耗费大量 CPU 时间,导致后台处理 Flush 的线程被"饿死"。
  4. 解决方案
    • HashMap 换成 LinkedHashMap(在我的业务中利用其 O ( 1 ) O(1) O(1) 的迭代器实现快速缓冲驱逐)。
    • 减小 MAX_BUCKET_SIZE,控制总对象数量。

结合火焰图,发现GC确实占用了大量的时间


最后:使用 jmap 的注意事项

  1. 生产环境慎用 :livejmap -histo:livejmap -dump:live 都会触发 Full GC。在堆内存超过 100GB 的系统上,这可能导致长达数十秒甚至几分钟的服务不可用。
  2. 权限问题 :请确保使用与 Java 进程相同的人员权限(通常是 sudo -u <user> jmap ...)执行。
  3. 版本差异 :从 JDK 9 开始,官方更推荐使用 jcmd <pid> GC.heap_dump 来代替 jmap,因为 jcmd 的性能开销通常更小。
相关推荐
想用offer打牌2 小时前
虚拟内存与寻址方式解析(面试版)
java·后端·面试·系统架构
json{shen:"jing"}2 小时前
2-C语言的运算符和表达式
c语言·开发语言
代码or搬砖2 小时前
SQL核心语法总结:从基础操作到高级窗口函数
java·数据库·sql
AI视觉网奇2 小时前
ue 虚幻引擎学习笔记
开发语言·虚幻引擎
月明长歌2 小时前
【码道初阶】【Leetcode94&144&145】二叉树的前中后序遍历(非递归版):显式调用栈的优雅实现
java·数据结构·windows·算法·leetcode·二叉树
ghie90902 小时前
使用MATLAB的k-Wave工具箱进行超声CT成像
开发语言·matlab
catchadmin2 小时前
PHP 8.6 新增 clamp() 函数
开发语言·php
杰克尼2 小时前
蓝桥云课-5. 花灯调整【算法赛】
java·开发语言·算法
.小墨迹2 小时前
C++学习之std::move 的用法与优缺点分析
linux·开发语言·c++·学习·算法·ubuntu