JVM实战:服务器堆外内存去哪了(NMT实测)

JVM 实战:-Xmx26g 的进程为什么吃掉 30G 内存?------用 NMT 算清堆外这笔账

前面 JVM 那篇讲堆内:内存持续上涨,最后发现是分配速率问题。这篇讲堆外:-Xmx26g 的游戏服进程,top 一看 RSS 30G 起步,多出来的几个 G 在哪?运维问"是不是泄漏了",你说"配置就这样"------本文用 NMT(Native Memory Tracking)把这笔账算清楚:进程内存 = 堆 + 元空间 + 线程栈 + 代码缓存 + GC 开销 + 直接内存 + ......每一项是什么、多大、怎么控制,全部配 13000 人压测的实测数据。

一、现象:堆只给了 26G,进程却吃了 30G

前面 JVM 那篇的启动参数里就有这么两行:

bash 复制代码
-Xms26g -Xmx26g
-XX:NativeMemoryTracking=summary

压测期间运维在监控上看到的现象:java 进程 RSS(常驻内存)稳定在 30G 上下,比堆大出 4G 左右。机器是按 32G 规划的,这 4G 的"计划外开支"不搞清楚,轻则容量规划做不准,重则堆外涨一波直接被 OOM killer 干掉(进程消失得悄无声息,前面 JVM 那篇附表里的第三种死法)。

先把结论放在这:这 4G 不是泄漏,是 JVM 正常的"堆外开支" 。一个 Java 进程的内存账本远不止 -Xmx:

复制代码
进程总内存(≈ top 的 RES)
  ├── Java Heap            ← -Xmx 管到的只有这块
  ├── Class(元空间)
  ├── Thread(线程栈)
  ├── Code(JIT 代码缓存)
  ├── GC(收集器的本地开销)
  ├── Compiler(JIT 编译器)
  ├── Internal(含 NIO 直接内存)
  ├── Symbol(符号表/常量池)
  ├── Arena Chunk(malloc 临时块)
  └── JVM 之外:JNI 库、glibc 缓存......

要看清这笔账,工具就是参数里已经埋下的 NMT。

二、NMT:把 JVM 的本地内存记账本打开

NativeMemoryTracking 三档:

档位 内容 开销
off(默认) 不记录 无
summary 按类目汇总 有额外性能开销(官方文档口径 5%~10%),排查期临时开
detail 记录到 malloc 调用栈 更大,定位疑难杂症用

我们的压测环境常开 summary(性能敏感的线上环境建议排查时再开)。用法就三个命令:

bash 复制代码
# 看总账
jcmd <pid> VM.native_memory summary

# 打个基线,跑一段时间
jcmd <pid> VM.native_memory baseline

# 和基线做差:这段时间哪些类目涨了
jcmd <pid> VM.native_memory summary.diff

baseline + diff 是 NMT 最有价值的用法------和 JProfiler 的 Mark Current 一个思路:不看绝对值,看增量。

三、逐项拆解:一份完整的 NMT 输出

下面是一次试跑环境的 NMT 输出(数值小,但条目齐全,适合逐项讲;生产量级看下一节的压测数据):

text 复制代码
Total: reserved=1907345KB, committed=1005237KB
-                 Java Heap (reserved=520192KB, committed=520192KB)
-                     Class (reserved=111285KB, committed=52904KB)
-                    Thread (reserved=233472KB, committed=29184KB)
-                      Code (reserved=254976KB, committed=18392KB)
-                        GC (reserved=136296KB, committed=48640KB)
-                  Compiler (reserved=24576KB, committed=13332KB)
-                  Internal (reserved=20636KB, committed=20636KB)
-                     Other (reserved=2048KB, committed=24KB)
-                    Symbol (reserved=26384KB, committed=26384KB)
-    Native Memory Tracking (reserved=8382KB, committed=8382KB)
-               Arena Chunk (reserved=30128KB, committed=30128KB)

每一项是什么、受什么控制:

1. Java Heap ------ -Xmx 管的那块,不多说。注意 NMT 里它只是十一个类目之一。

2. Class(元空间) ------ 类的元数据。受 -XX:MaxMetaspaceSize 限制,不设上限默认可以吃到物理内存满 (所以这行参数永远该显式写)。看什么业务会让它涨:动态生成类。游戏服里的重灾区是脚本/热更/反射-heavy 的框架------CGLib 代理、Groovy、每张配置表一个动态类,都会把类数量顶上去。

3. Thread(线程栈) ------ 每线程一块,大小由 -Xss 定(64 位 Linux 默认 1M)。这笔账可以手算:线程数 × -Xss。注意一个细节:NMT 里 Thread 的 committed 通常明显小于 线程数 × -Xss------栈是按页提交的,线程没碰到那么深,页就没提交。reserved 才是你的"承诺开支"。

4. Code(JIT 代码缓存) ------ 热点代码编译成的本地指令。reserved 主要就是 -XX:ReservedCodeCacheSize(默认 240M)划的地盘。代码缓存写满后 JIT 停止编译,性能会突然掉一截------大流量服务建议盯着 jconsole/JMX 的 CodeCache 用量。

5. GC ------ 收集器自己的工作内存 ,很容易被忽略的大头。G1 的卡表、remembered set、标记位图全在本地内存里,堆越大这块越大。我们 26G 的堆,这块压测时到了 676M(见下节)------换收集器、调堆大小时,这笔账要跟着重算。

6. Compiler ------ JIT 编译线程编译期间的临时内存,量级小(压测时 1M 级别),一般不用管。

7. Internal ------ 命令行解析、JVMTI 等 JVM 内部结构。重点提醒:JDK 8 里 NIO 的 DirectByteBuffer(直接内存)通常也计入这一类 (部分版本记在 Other)。游戏服用 Netty,堆外缓冲走的就是这条路------Internal 异常大的时候,第一个查 NIO/Netty 的直接内存。直接内存的上限是 -XX:MaxDirectMemorySize,不显式设置时默认约等于 -Xmx------26G 的堆配 26G 的直接内存上限,等于没有护栏。

8. Symbol ------ 常量池符号、字符串表,量级小。

9. Native Memory Tracking ------ NMT 自己的开销,也要记账(约 5~9M)。工具自身有成本,这也是它默认关闭的原因。

10. Arena Chunk ------ malloc 分配的临时内存块,给线程做 per-thread 临时分配用,成批释放(不是即用即还)。这块和第五节的 glibc 行为直接相关,后面细说。

11. Other ------ 尚未归类的杂项。

排查技巧:NMT 有一块天然盲区------它只记 JVM 自己(os::malloc)申请的内存。你自己 JNI 库(比如前面 CPU 排查那篇的 xLua 校验库)、第三方 native 依赖(压缩库、加密库)直接 malloc 的内存,NMT 里看不到。RSS 和 NMT 总账对不上的差额,先怀疑这类。

四、生产量级:13000 人压测的实测账本

试跑环境的数字没有震撼力,这张表是压测(13000 个机器人、3 秒请求间隔、持续 5~6 小时)时的实测值:

类目 压测实测 对应参数/说明
堆 26G -Xms26g -Xmx26g
元空间 82M(11132 个类) 上限 256M(MaxMetaspaceSize)
线程栈 120M(211 个线程) -Xss512k,211 × 512K ≈ 105M,对得上
Code 保留 259M,实际 81M 保留≈代码缓存地盘,实际用 81M
GC 676M 26G 大堆的 G1 工作内存
Compiler 1M 可忽略
Internal 480M 大头是 NIO 直接内存(Netty)
Arena Chunk 63M malloc 临时块

堆外各项加总 1.5G 左右,再算上 JNI 库、glibc 的缓存和碎片开销,正好落在"堆外 2~3G"的规划口径里------26G 的堆,进程预算就该按 29~30G 报。这笔账算清之后,"RSS 比 Xmx 大 4G"从一个问题变成一句常识。

五、三个实战高频问题

1)reserved、committed、RSS,到底看哪个?

三个数是三层口径:

复制代码
reserved:JVM 向 OS 预定的地址空间(画饼)
committed:JVM 已向 OS 要到的内存(真金白银要了)
RSS:OS 角度实际驻留物理内存的页(真花了物理内存)

reserved ≥ committed ≥ 通常 RSS(committed 里没被写过的页不占物理内存)。压测时 Code 保留 259M、committed 81M,就是典型:地盘画了 240M+,实际用 81M。容量规划看 committed,泄漏排查看 RSS 的增量,别拿 reserved 吓自己。

2)RSS 涨上去不下来,是泄漏吗?

多数时候不是。两个"合法原因":

  • glibc 的 malloc arena 机制 :glibc 默认给每个核创建最多 8 个 arena,多线程程序的内存在各 arena 间分散,free 之后内存倾向于留在 arena 里复用,而不是还给 OS ------表现出来就是 RSS 只涨不跌。游戏服线程多,这个现象很常见。经典缓解:启动前设置 MALLOC_ARENA_MAX=2(或 4),或者直接换 jemalloc/tcmalloc;
  • JVM 归还内存的时机很保守 :JDK 8 的 G1 基本不主动把 committed 的堆还给 OS(JDK 12+ 才有周期性 uncommit 的选项)。-Xms=-Xmx 的写法本身也表明你不想让它还。

真泄漏的判别方法还是前面 JVM 那篇的方法论:看趋势,不看单点------RSS 平台期波动是正常,单调爬升两个月不停才是病。

3)堆外会 OOM 吗?会,而且死法更隐蔽

  • 直接内存超 MaxDirectMemorySize:抛 OutOfMemoryError: Direct buffer memory,Netty 场景高发(池化 buffer 泄漏、没 release);
  • 进程总内存(堆+堆外)顶到机器上限:OS 直接 kill,无任何日志 ,只在 /var/log/messages 里留一行 OOM killed------这是堆外预算必须留余量的根本原因。

排查直接内存的手段:NMT 的 Internal/Other 类目(JDK 8 口径)、Netty 自带的 PooledByteBufAllocator 指标、-Dio.netty.leakDetection.level=paranoid(排查期开)。

六、复盘

问题 之前的认知 应有的认知
进程 30G,堆 26G,差 4G "Java 真吃内存"/"是不是泄漏" 堆外是正常开支,可以逐项算清
容量规划 按 Xmx 报机器 Xmx + 元空间 + 线程栈 + Code + GC + 直接内存 + 余量
RSS 高居不下 怀疑泄漏 glibc arena + JVM 保守归还,先看趋势再定性
NMT 总账 < RSS "NMT 不准" NMT 不含 JNI/第三方库的 malloc,差额先查这些

方法论一句话:给进程记一本账,三个工具各管一段 ------top/pmap 看 RSS 总量,NMT 看 JVM 内部分账,差额归 native 库和 glibc。哪段异常查哪段,别拿着 Xmx 一根尺子量所有内存。

七、写在最后

这一篇和堆内那篇合成一个完整的内存视角:堆内看分配速率,堆外看账本明细。两篇看完,"Java 进程到底吃多少内存、吃在哪、为什么降不回去"这类问题应该都能拆解作答。


相关推荐
vipxieliang2 小时前
PHP 依赖注入容器从零实现:控制反转、手动注入与容器管理全解析
后端·php
智码看视界2 小时前
Day84-Arthas诊断实战:CPU飙高/死锁/内存泄漏排查三板斧
jvm·内存泄漏·arthas·死锁·性能诊断·生产调优
fundoit2 小时前
OIDC的UserInfo端点
java·架构·oauth2·oidc
运维行者_2 小时前
PHP性能监控怎么做?从响应时间到慢函数的6个关键指标
运维·服务器·开发语言·网络·支持向量机·php·接口隔离原则
Wang's Blog2 小时前
Java框架 SpringCloud 快速入门: Nacos 配置管理之添加配置与 dataId 命名规范
java·开发语言·spring cloud
JAVA面经实录9172 小时前
Java高级后端 · 全套面试通关手册(Nginx)
java·nginx·面试
阿俊-全栈开发2 小时前
LikeShop单商户Java商城如何从容承接高并发流量?
java·开发语言·spring boot·spring·系统架构
Java小白笔记2 小时前
Java 函数式接口1:从无参任务到自定义多参数查询
java·windows·python
小HANN2 小时前
华为云企业网站上云实战|从零搭建高可用WordPress(ECS+RDS+ELB+弹性伸缩+云监控全流程落地)
linux·运维·服务器·经验分享