liuxiaoshui
Published on 2025-03-25 / 7 Visits
0
0

spring cloud gateway 内存占用

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 后,输出将包含以下详细信息:

```


Comment