docker stats内存统计分析:
原因分析
堆外内存未控制
JVM 的 -Xmx 仅限制堆内存(Heap Memory),但还存在:
元空间(Metaspace)默认不限制(需通过 -XX:MaxMetaspaceSize 限制)
直接内存(Direct Memory)默认与 -Xmx 同值(需通过 -XX:MaxDirectMemorySize 限制)
JVM 自身数据结构(线程栈、GC 数据结构、JIT 编译缓存等)
Netty 内存池机制(核心因素)
Spring Cloud Gateway 基于 Netty 实现,Netty 默认使用 PooledByteBufAllocator
其直接内存池默认分配值 = Runtime.getRuntime().maxMemory()(即与 -Xmx 同值)
未显式配置时会导致额外占用约 6GB 直接内存
容器内存统计机制
docker stats 显示的是容器 总内存占用(RSS + Page Cache + Swap)
包含:
JVM 所有内存区域(堆 + 非堆)
子进程内存(如 shell 进程)
内核文件缓存(Page Cache)
定位步骤
1. 检查 JVM 实际内存分布
jcmd 1 VM.native_memory summary
返回:
- Java Heap (reserved=6GB, committed=6GB)
- Class (reserved=xxxMB, committed=xxxMB) # Metaspace
- Thread (reserved=xxxMB, committed=xxxMB) # 线程栈
- Code (reserved=xxxMB, committed=xxxMB) # JIT 编译代码
- GC (reserved=xxxMB, committed=xxxMB) # GC 数据结构
- Internal (reserved=xxxMB, committed=xxxMB) # 直接内存
root@HyServer1:/data/hengyu_framework# jcmd 1 VM.native_memory summary
1:
Native Memory Tracking:
Total: reserved=2955696KB, committed=1554344KB
- Java Heap (reserved=1048576KB, committed=1048576KB)
(mmap: reserved=1048576KB, committed=1048576KB)
- Class (reserved=1130105KB, committed=92913KB)
(classes #18197)
( instance classes #17224, array classes #973)
(malloc=3705KB #52363)
(mmap: reserved=1126400KB, committed=89208KB)
( Metadata: )
( reserved=77824KB, committed=77056KB)
( used=74897KB)
( free=2159KB)
( waste=0KB =0.00%)
( Class space:)
( reserved=1048576KB, committed=12152KB)
( used=10822KB)
( free=1330KB)
( waste=0KB =0.00%)
- Thread (reserved=179604KB, committed=18036KB)
(thread #174)
(stack: reserved=178780KB, committed=17212KB)
(malloc=626KB #1046)
(arena=198KB #342)
- Code (reserved=251247KB, committed=48655KB)
(malloc=3563KB #13128)
(mmap: reserved=247684KB, committed=45092KB)
- GC (reserved=103612KB, committed=103612KB)
(malloc=31536KB #25063)
(mmap: reserved=72076KB, committed=72076KB)
- Compiler (reserved=536KB, committed=536KB)
(malloc=399KB #1525)
(arena=136KB #9)
- Internal (reserved=1673KB, committed=1673KB)
(malloc=1641KB #4459)
(mmap: reserved=32KB, committed=32KB)
- Other (reserved=198253KB, committed=198253KB)
(malloc=198253KB #163)
- Symbol (reserved=18917KB, committed=18917KB)
(malloc=17790KB #216605)
(arena=1127KB #1)
- Native Memory Tracking (reserved=5070KB, committed=5070KB)
(malloc=34KB #430)
(tracking overhead=5036KB)
- Shared class space (reserved=17036KB, committed=17036KB)
(mmap: reserved=17036KB, committed=17036KB)
- Arena Chunk (reserved=221KB, committed=221KB)
(malloc=221KB)
- Logging (reserved=4KB, committed=4KB)
(malloc=4KB #192)
- Arguments (reserved=18KB, committed=18KB)
(malloc=18KB #473)
- Module (reserved=397KB, committed=397KB)
(malloc=397KB #3011)
- Synchronizer (reserved=419KB, committed=419KB)
(malloc=419KB #3475)
- Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB)
当执行 jcmd <PID> VM.native_memory summary 时出现 "Native memory tracking is not enabled" 错误,说明 JVM 未启用 Native Memory Tracking (NMT) 功能。以下是解决方案:
步骤 1:启用 NMT 功能
在 JVM 启动参数中添加以下参数重启应用:
-XX:NativeMemoryTracking=summary # 基础模式(推荐)
# 或
-XX:NativeMemoryTracking=detail # 详细模式(性能开销略高)
示例(Docker 环境):
# Dockerfile 中配置
CMD ["java", "-XX:NativeMemoryTracking=summary", "-Xms6g", "-Xmx6g", "-jar", "your-app.jar"]
# 或 docker run 命令中传递
docker run -e JAVA_OPTS="-XX:NativeMemoryTracking=summary -Xms6g -Xmx6g" ...
步骤 2:验证 NMT 是否生效
应用重启后,执行以下命令验证:
# 进入容器
docker exec -it <container_id> /bin/bash
# 查看 JVM 进程 ID
jps -l
# 检查 NMT 状态
jcmd <PID> VM.native_memory summary
注意事项
性能影响 NMT 会引入约 5-10% 的性能开销(detail 模式更高),生产环境建议短期诊断后关闭。
容器权限 确保容器有执行 jcmd 的权限,若使用精简镜像(如 alpine),需安装 openjdk-debug 包。
配置同步 若使用 Kubernetes,需通过环境变量传递 JVM 参数:
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:NativeMemoryTracking=summary"
关联知识
NMT 数据解读重点区域:
- Java Heap # 堆内存(受 -Xmx 控制)
- Class # 类元数据(Metaspace)
- Thread # 线程栈(与线程数正相关)
- Code # JIT 编译代码缓存
- GC # GC 数据结构
- Internal # 直接内存(Direct Buffer)
- Symbol # 符号表
若发现 Internal 或 Class 区域异常增长,需检查 Direct Memory 使用或 Metaspace 泄漏。
在 JDK 11 中,若只配置 -Xms6g 和 -Xmx6g,其他内存区域会按以下规则自动配置:
一、主要内存区域及默认行为
1. 元空间(Metaspace)
默认行为
无上限(默认不限制,但受物理内存/容器限制约束)
初始大小约 20MB,根据类加载情况自动扩展
通过 -XX:MaxMetaspaceSize 可限制最大值
风险点 动态加载大量类(如频繁热部署)可能导致元空间内存持续增长
2. 直接内存(Direct Memory)
默认行为
最大值默认等于 -Xmx(即 6GB)
通过 -XX:MaxDirectMemorySize 显式控制
关键影响 Netty 等 NIO 框架会使用直接内存池(如未显式配置 -Dio.netty.maxDirectMemory)
3. 线程栈(Thread Stack)
默认行为
Linux x64 默认 1MB/线程(通过 -Xss 调整)
总内存 = 线程数 × 1MB
典型场景 高并发场景(如 1000 线程)可能占用 1GB 内存
4. JIT 代码缓存(Code Cache)
默认行为
初始 240MB,最大约 480MB(通过 -XX:ReservedCodeCacheSize 控制)
风险点 大型应用或高频动态编译可能导致代码缓存占满
5. GC 数据结构
默认行为
G1 GC 默认占用堆大小的约 10%(即 6GB 堆对应约 600MB)
通过 -XX:+UseParallelGC 可减少开销
6. 本地库内存
包含内容 JNI 调用、压缩指针(Compressed Oops)表等
典型占用 约 100-200MB
二、典型内存分布(估算)
三、容器环境特殊行为
1. 容器内存感知
JDK 8u191+ 和 JDK 10+ 默认支持容器内存限制
关键参数 -XX:+UseContainerSupport(JDK 11 默认启用)
风险点 若未显式配置元空间/直接内存限制,仍可能触发容器 OOM
2. Page Cache 影响
文件读写操作会增加 docker stats 显示的缓存内存(非 JVM 实际占用)
四、推荐显式配置
# 基础内存限制
-Xms6g -Xmx6g
-XX:MaxMetaspaceSize=512m # 限制元空间
-XX:MaxDirectMemorySize=2g # 限制直接内存
-XX:ReservedCodeCacheSize=240m # 代码缓存上限
# GC 优化
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# 线程栈优化(按需调整)
-Xss256k # 降低单线程栈大小
# Netty 专用控制(如使用 Spring Cloud Gateway)
-Dio.netty.maxDirectMemory=0 # 禁用 Netty 独立直接内存池
-Dio.netty.allocator.type=unpooled # 禁用内存池
五、验证方法
1、容器内内存监控
docker stats <container_id> # 观察容器总内存
top -p <PID> # 查看进程 RES 内存
2、NMT 内存分析
jcmd <PID> VM.native_memory summary # 需先启用 NMT
3、堆外内存工具
jcmd <PID> VM.metaspace # 元空间详情
jmap -histo <PID> # 检查 DirectByteBuffer 对象
通过显式配置非堆内存参数,可将总内存控制在 6G(Heap) + 2G(Direct) + 0.5G(Metaspace) + 其他 < 10G 范围内。
question:
保留内存池但限制大小
-Dio.netty.allocator.maxOrder=3 # 降低内存块尺寸(默认11每个chunk 8MB改为3每个chunk 512KB) -Dio.netty.allocator.numHeapArenas=2 # 堆内存区数量(根据CPU核数调整) -Dio.netty.allocator.numDirectArenas=2 # 直接内存区数量 -XX:MaxDirectMemorySize=4g
还需要设置-Dio.netty.maxDirectMemory=0么?
在配置 Netty 内存池时,-Dio.netty.maxDirectMemory=0 参数的作用是禁用 Netty 的独立直接内存管理机制,转而使用 JVM 的 -XX:MaxDirectMemorySize 限制。如果你已经通过 -XX:MaxDirectMemorySize=4g 明确限制了直接内存的总量,并且希望保留 Netty 的内存池机制但进行合理控制,那么不需要再设置 -Dio.netty.maxDirectMemory=0。
原因分析
Netty 内存池与 JVM 直接内存的关系
Netty 默认会根据 -Xmx 设置来确定其直接内存池的最大值(即默认等于堆内存最大值)。例如,如果 -Xmx 设为 6g,Netty 将预留最多 6GB 的直接内存。
如果设置了 -XX:MaxDirectMemorySize=4g,JVM 级别的直接内存限制将生效,Netty 的直接内存池也会受到这个限制的影响。
-Dio.netty.maxDirectMemory=0 的作用
当设置 -Dio.netty.maxDirectMemory=0 时,Netty 不再独立管理直接内存池,而是完全依赖于 JVM 的 -XX:MaxDirectMemorySize 设置。
这意味着 Netty 将不再预分配和管理大块的直接内存,而是每次需要时从 JVM 分配小块内存,这可能会增加内存分配和释放的开销。
保留内存池并限制大小
如果你希望通过 Netty 的内存池机制来优化内存管理和减少内存碎片,同时又希望限制直接内存的总量,可以只设置 -XX:MaxDirectMemorySize=4g,而不设置 -Dio.netty.maxDirectMemory=0。
这样,Netty 仍然会使用其内存池机制,但在总体上受到 JVM 直接内存限制的约束。
推荐配置
# 限制 JVM 直接内存总量
-XX:MaxDirectMemorySize=4g
# 控制 Netty 内存池参数
-Dio.netty.allocator.maxOrder=3 # 降低内存块尺寸(每个chunk 512KB)
-Dio.netty.allocator.numHeapArenas=2 # 堆内存区数量(根据CPU核数调整)
-Dio.netty.allocator.numDirectArenas=2 # 直接内存区数量
总结
不需要设置 -Dio.netty.maxDirectMemory=0:因为 -XX:MaxDirectMemorySize=4g 已经足够限制直接内存的总量,同时保留 Netty 的内存池机制可以带来更好的性能和内存管理效果。
保留内存池并限制大小:通过上述配置,可以在保证性能的同时,有效控制直接内存的使用量
在配置 Netty 内存池时,合理设置 -Dio.netty.allocator.maxOrder、-Dio.netty.allocator.numHeapArenas 和 -Dio.netty.allocator.numDirectArenas 参数对于优化内存管理和性能至关重要。以下是如何设置这些参数以及如何评估其大小的详细说明:
一、参数解释与设置
1. 降低内存块尺寸:-Dio.netty.allocator.maxOrder=3
含义:限制 Netty 内存池中分配的最大内存块大小。
默认值为 11(每个 chunk 大小为 8MB)
设置为 3 后,最大 chunk 大小变为 512KB
作用:
减少大块内存分配导致的碎片化问题
更适合处理大量小数据包的场景
2. 堆内存区数量:-Dio.netty.allocator.numHeapArenas=2
含义:指定 Netty 在堆内存中创建的内存区域(Arena)数量。
推荐设置:
根据 CPU 核数调整,一般设置为 CPU 核数 * 2
示例:4 核 CPU 可设置为 8
3. 直接内存区数量:-Dio.netty.allocator.numDirectArenas=2
含义:指定 Netty 在直接内存中创建的内存区域(Arena)数量。
推荐设置:
同样根据 CPU 核数调整,一般设置为 CPU 核数 * 2
示例:4 核 CPU 可设置为 8
4. JVM 直接内存限制:-XX:MaxDirectMemorySize=4g
含义:限制 JVM 使用的直接内存总量。
推荐设置:
根据应用实际需求和系统资源情况调整
避免设置过高导致 OOM 或过低影响性能
二、评估与调优方法
1. 监控内存使用情况
工具:jcmd、jstat、VisualVM 等
关键指标:
直接内存使用量(通过 jcmd <PID> VM.native_memory summary 查看)
堆内存使用量(通过 jstat -gc <PID> 查看)
2. 分析内存分配模式
日志记录:启用 Netty 内存泄漏检测
-Dio.netty.leakDetection.level=paranoid
检查日志:查找未释放的 ByteBuf 对象,定位潜在内存泄漏点
3. 压力测试验证
工具:JMeter、Wrk 等
测试场景:
模拟高并发请求
观察内存占用变化趋势
记录吞吐量和响应时间等性能指标
4. 动态调整参数
逐步调整:从默认值开始,逐步调整参数观察效果
对比测试:不同配置下进行多轮测试,对比性能差异
三、示例配置
假设当前环境为 4 核 CPU,推荐配置如下:
# 控制 Netty 内存池参数
-Dio.netty.allocator.maxOrder=3 # 降低内存块尺寸(每个chunk 512KB)
-Dio.netty.allocator.numHeapArenas=8 # 堆内存区数量(4核*2)
-Dio.netty.allocator.numDirectArenas=8 # 直接内存区数量(4核*2)
# 限制 JVM 直接内存总量
-XX:MaxDirectMemorySize=4g # 根据应用需求调整
四、注意事项
避免过度细分 过小的内存块可能导致频繁的内存分配/回收,增加 GC 开销。
考虑业务特征 不同业务场景对内存管理的需求不同,需结合实际流量特征调整参数。
定期复审配置 随着业务发展和系统负载变化,定期复审并调整内存池配置以保持最优状态。
五、总结
通过合理设置 Netty 内存池参数,并结合监控和压力测试手段,可以有效控制内存使用量,提升系统的稳定性和性能。
docker示例查看内存及swap配置:
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
# 查找容器的 cgroup 内存子系统路径(需替换容器ID)
CONTAINER_ID="your_container_id"
CGROUP_PATH=$(cat /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/cgroup.procs | head -1)
cd /sys/fs/cgroup/memory/docker/${CONTAINER_ID}/
# 查看内存和 swap 限制
cat memory.limit_in_bytes # 物理内存限制(单位:字节)
cat memory.memsw.limit_in_bytes # 物理内存 + swap 总限制(单位:字节)
1. 配置含义
该文件记录了当前 cgroup 的内存使用限制值(单位为字节),包括:
物理内存 + 交换空间(swap)的总限制
若进程内存使用超过此值,会触发 OOM Killer 终止进程
2. 输出值解释
你看到的 9223372036854771712 是 系统默认的无限制标识,其数值等价于:
2^63 - 4096 字节(即接近 9,223,372,036,854,775,808)
约等于 8 Exabytes(现实场景中不可能达到)
这表示:
当前 cgroup 未显式配置内存限制
实际可用内存取决于宿主机物理内存或更高层级的 cgroup 限制
3. 典型场景
常见于:
Docker/Kubernetes 容器未设置 --memory 参数时
物理机直接运行的进程默认继承宿主机的内存资源
4. 如何显式设置限制?
若需限制内存,可向该文件写入具体数值(如限制为 1GB):
echo 1073741824 > /sys/fs/cgroup/memory/memory.limit_in_bytes
直接内存泄漏:
现象:Other 占用 2GB,需检查代码中 DirectByteBuffer 是否正确释放。
工具:使用 jcmd <pid> VM.native_memory detail 或 jemalloc 分析。
要启用详细的 Native Memory Tracking (NMT) 以获取 jcmd VM.native_memory detail 的完整输出,需通过以下步骤配置 JVM 参数:
1. 启用 NMT 详细跟踪
在 JVM 启动时添加参数 -XX:NativeMemoryTracking=detail,例如:
java -XX:NativeMemoryTracking=detail -jar your_app.jar
参数说明
-XX:NativeMemoryTracking=summary:默认级别(仅汇总统计)
-XX:NativeMemoryTracking=detail:启用详细内存跟踪(需重启 JVM)
2. 生成详细内存报告
启用后,通过以下命令获取详细信息:
jcmd <PID> VM.native_memory detail
# 示例(假设 PID 为 1):
jcmd 1 VM.native_memory detail
3. 可选操作:生成内存快照对比
步骤 1:记录基线
jcmd <PID> VM.native_memory baseline
步骤 2:运行一段时间后生成差异报告
jcmd <PID> VM.native_memory detail.diff
4. 关闭 NMT(可选)
诊断完成后,关闭 NMT 以减少性能开销:
jcmd <PID> VM.native_memory shutdown
5. 输出示例
启用 detail 后,输出将包含以下详细信息:
```