JVM 内存分析实战:40MB 程序为什么占用了 180MB 内存?

一、背景

华为云服务器,配置 1.7GB 内存,部署了一个 Spring Boot 单体应用(blog-server-1.0.0.jar,程序包大小约 40MB)。应用使用 OpenJDK 1.8 运行,目前没有业务访问量。

通过 top 命令看到 Java 进程占用了约 180MB 物理内存。40MB 的 jar 包,为什么吃掉 180MB 内存?这 180MB 里面到底有什么?

于是开始了一次从外到内的 JVM 内存分析。

二、测试环境

项目 信息
云服务商 华为云 ECS
操作系统 Ubuntu 24.04.3 LTS
总内存 1.7 GiB
Java 版本 OpenJDK 1.8.0_482
应用 blog-server-1.0.0.jar(Spring Boot)
JVM 参数 -Xms64M -Xmx256M -XX:NativeMemoryTracking=summary

三、分析过程

第一步:查看服务器整体内存

free -h
               total        used        free      shared  buff/cache   available
Mem:           1.7Gi       746Mi       155Mi       2.6Mi       1.0Gi       1.0Gi

服务器整体可用内存约 1.0GB,压力不大。

第二步:查看 Java 进程资源占用

top -p 1087963
PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
1087963 root   20   0 2652448 190500  18688 S   1.3  10.5   0:09.85 java
  • RES = 190500KB ≈ 186MB,这就是要分析的 180MB
  • VIRT = 2.65GB(虚拟内存大是正常的,JVM 会预留地址空间)

第三步:用 jstat 查看堆内存

jstat -gc 1087963
 S0C    S1C    S0U    S1U      EC       EU        OC       OU       MC     MU    YGC  YGCT   FGC  FGCT
2176.0 2176.0 1638.3  0.0   17536.0   6188.5   43712.0  22471.7  54528.0 51169.3  48  0.206   2   0.055

计算:

  • 堆已用 = S0U(1.6MB) + EU(6.0MB) + OU(21.9MB) ≈ 29.5MB
  • 元空间已用 = MU ≈ 50.0MB
  • 堆 + 元空间 ≈ 80MB

新问题:186MB - 80MB = 106MB,这 106MB 去哪了?

第四步:开启 NMT 查看完整内存分布

在启动参数中加入 -XX:NativeMemoryTracking=summary,重启后执行:

jcmd 1087963 VM.native_memory summary

输出:

Total: reserved=1662343KB, committed=186727KB

-                 Java Heap (committed=65600KB)   # 64.1 MB
-                     Class (committed=56621KB)   # 55.3 MB(元空间)
-                    Thread (committed=28798KB)   # 28.1 MB
-                      Code (committed=17071KB)   # 16.7 MB(代码缓存)
-                    Symbol (committed=13911KB)   # 13.6 MB(符号表)
-      Native Memory Tracking (committed=2356KB)  # 2.3 MB
-         其他(GC/Internal等)                  # ~3.0 MB

完整内存分布:

内存区域 大小 占比
Java 堆 64 MB 34.4%
元空间 (Class) 55 MB 29.7%
线程栈 28 MB 15.1%
代码缓存 17 MB 9.0%
符号表 14 MB 7.3%
其他 ~5 MB 2.8%
总计 ~186 MB 100%

第五步:补充验证

查看线程数:

jcmd 1087963 Thread.print | grep -c "^\""
# 输出:28

确认 JVM 参数:

jcmd 1087963 VM.flags
-XX:InitialHeapSize=67108864 -XX:MaxHeapSize=268435456 -XX:NativeMemoryTracking=summary ...

查看系统环境:

cat /etc/os-release
# Ubuntu 24.04.3 LTS

java -version
# openjdk version "1.8.0_482"

第六步:生成 Heap Dump

jmap -dump:live,format=b,file=/tmp/heap.hprof 1087963
ls -lh /tmp/heap.hprof
# -rw-r--r-- 1 root root 40M  heap.hprof

生成 40MB 的 dump 文件,可用 VisualVM 或 MAT 离线分析堆内对象构成。

四、JDK 工具对比总结

工具 用途 适用场景
jstat 查看堆内存、GC 统计 日常监控
jcmd 多功能诊断(NMT、线程、类统计) 功能最全面
jmap 生成 Heap Dump 深入分析堆内对象
jstack 查看线程堆栈 排查死锁、线程卡顿
jconsole 图形化监控 可视化看内存/线程曲线
jhat 分析 Heap Dump 已过时,用 VisualVM/MAT 替代

五、结论

  1. -Xmx 只控制堆的最大容量,元空间、线程栈、代码缓存、符号表等堆外内存不受它限制。

  2. Spring Boot 应用的元空间占用较高,即使堆只有 64MB,元空间也会因为加载近万个类而占用 50+MB。

  3. 186MB 对于小型 Spring Boot 应用是正常的内存占用,不是内存泄漏。

  4. NMT(Native Memory Tracking)是分析堆外内存的有效工具,建议在启动参数中开启。

六、后续工具延伸

如果遇到更复杂的内存问题(堆外内存泄漏、高 CPU 问题定位),JDK 自带工具可能不够用。阿里巴巴开源的 Arthas 提供了更强大的能力:

  • memory 命令:一键查看所有内存区域
  • 堆外内存追踪
  • profiler 命令:生成火焰图进行方法级热点分析
  • jad 命令:在线反编译运行时代码
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐