文章目录
-
- [什么是 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 实际分配的最大堆内存是否符合预期。
- Old Generation :如果
示例:
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。
- 现象:RocksDB 的Flush P50 延迟高达 3 秒。
- 通过 jmap 发现:老年代几乎被这些 Node 对象填满。
- 推论:由于老年代对象过多,JVM 在进行 G1 扫描(Scan Roots)时需要耗费大量 CPU 时间,导致后台处理 Flush 的线程被"饿死"。
- 解决方案 :
- 将
HashMap换成LinkedHashMap(在我的业务中利用其 O ( 1 ) O(1) O(1) 的迭代器实现快速缓冲驱逐)。 - 减小
MAX_BUCKET_SIZE,控制总对象数量。
- 将
结合火焰图,发现GC确实占用了大量的时间

最后:使用 jmap 的注意事项
- 生产环境慎用
:live:jmap -histo:live和jmap -dump:live都会触发 Full GC。在堆内存超过 100GB 的系统上,这可能导致长达数十秒甚至几分钟的服务不可用。 - 权限问题 :请确保使用与 Java 进程相同的人员权限(通常是
sudo -u <user> jmap ...)执行。 - 版本差异 :从 JDK 9 开始,官方更推荐使用
jcmd <pid> GC.heap_dump来代替jmap,因为jcmd的性能开销通常更小。