一、内存使用率不高,系统为什么还是会卡
排查服务器性能问题时,我们通常会先执行:
free -h
top
vmstat 1
如果是 Java 服务,还会继续检查堆内存、GC 次数和停顿时间。
但线上经常出现一种看起来很矛盾的情况:
- 服务器没有发生 OOM;
- CPU 使用率也不高;
- JVM Full GC 并不频繁;
- 内存还有少量可用空间;
- 接口的 P99 延迟却周期性升高。
原因可能不在“内存是否用完”,而在于系统已经开始为寻找可回收页面付出代价。
当可用内存逐渐减少时,Linux 需要扫描页面、调整 LRU 链表、回写脏页或者回收冷页。回收不及时,业务线程还可能进入直接内存回收路径。此时即使 CPU 曲线不高,线程也可能因为内存和 IO 资源争用而停顿。
Linux 的 PSI 可以量化 CPU、内存和 IO 资源不足造成的任务停顿时间,但 PSI 主要回答的是“系统因为资源压力停顿了多久”,并不能直接指出“究竟是哪部分内存长期占着却很少访问”。(Linux内核文档)
于是,内存排障还缺少几个关键答案:
- 进程常驻的内存中,哪些区域正在被频繁访问?
- 哪些区域已经很久没有发生有效访问?
- 有多少内存只是占着物理页面,却并不属于当前工作集?
- 能否在真正发生高压回收之前,提前处理这些冷内存?
这正是 Linux DAMON 要解决的问题。
二、DAMON 是什么
DAMON 全称为 Data Access MONitoring and Access-aware System Operations,它是 Linux 内核中的数据访问监控与访问感知操作子系统。
它并不只是统计一个进程占用了多少内存,而是持续观察内存区域的访问模式,记录这些区域:
- 有多大;
- 被访问得有多频繁;
- 当前访问模式维持了多久;
- 属于虚拟地址空间还是物理地址空间;
- 是否可能成为冷内存候选。
DAMON 的目标是让数据访问监控同时具备准确、轻量、可扩展、可调节和可自动化等特征,从而能够用于在线生产环境,而不只是离线性能分析。
DAMON 当前提供了三类主要监控操作集:
| 操作集 | 监控对象 | 典型场景 |
|---|---|---|
vaddr |
指定进程的虚拟地址空间 | 分析 Java、数据库、缓存服务 |
fvaddr |
固定虚拟地址范围 | 分析明确的映射区域 |
paddr |
系统物理地址空间 | 系统级冷热内存管理 |
DAMON 把底层地址空间访问检查、核心采样逻辑和上层管理模块进行了分层。这样既可以监控单个进程,也可以从整个系统物理内存的角度进行分析。
三、DAMON 和传统内存工具有什么区别
不同工具回答的是不同问题。
| 工具或指标 | 主要回答的问题 | 局限 |
|---|---|---|
free、MemAvailable |
系统还有多少可用内存 | 不知道内存是否活跃 |
| RSS、PSS | 进程常驻了多少物理内存 | RSS 高不代表这些页面正在使用 |
| JVM Heap、NMT | Java 堆和本地内存由什么组成 | 不直接反映页面访问冷热度 |
| PSI | 资源压力造成了多少停顿 | 能发现压力,但不能定位冷区域 |
perf |
哪段代码、哪类硬件事件最活跃 | 更偏向 CPU 和代码热点分析 |
| DAMON | 哪些内存区域多大、访问多频繁、模式维持多久 | 采样结果不是对象级或逐页绝对精确值 |
可以把这些工具理解成不同层次的摄像头:
- RSS 告诉你仓库里放了多少货;
- PSI 告诉你仓库入口堵了多久;
perf告诉你哪些工作人员最忙;- DAMON 告诉你哪些货架一直在周转,哪些货架很久没人碰。
因此,DAMON 并不是用来替代 free、PSI、JFR 或 perf,而是补上“内存访问模式”这一层。
四、DAMON 为什么能够保持较低开销
1. 不逐页持续监控
最直接的内存访问监控方式,是检查每一个页面是否被访问。
问题是,当服务器拥有几十 GB、几百 GB 甚至数 TB 内存时,逐页持续扫描的开销会快速增加,监控本身反而可能影响业务。
DAMON 采用的是区域化采样。
它把相邻且访问特征相似的页面归为一个区域。每个采样周期只从区域中选择一个页面检查访问情况,再把结果记入整个区域的访问计数。
因此,DAMON 的开销更多取决于监控区域数量,而不是直接随着物理页面总数无限增长。(Linux内核文档)
2. 动态拆分与合并区域
业务的内存访问模式会持续变化。
某个连续内存区域最初可能都很活跃,运行一段时间后,其中一部分页面可能变冷。如果继续把整个区域视为一个整体,结果就会失真。
DAMON 会根据相邻区域的访问频率动态调整边界:
- 访问模式接近的相邻区域可以合并;
- 区域内部出现明显差异时可以拆分;
- 区域总数被限制在用户配置的上下限内;
- 在监控精度和运行开销之间保持可控平衡。
其核心流程如下:
flowchart LR
A["确定监控地址范围"] --> B["划分初始内存区域"]
B --> C["每个区域选择采样页面"]
C --> D["检查页面是否被访问"]
D --> E["累计区域访问次数"]
E --> F["聚合访问结果"]
F --> G{"相邻区域特征是否接近"}
G -->|"接近"| H["合并区域"]
G -->|"差异明显"| I["拆分区域"]
H --> C
I --> C
Linux 内核文档将这一机制称为 Region Based Sampling 和 Adaptive Regions Adjustment。(Linux内核文档)
五、理解 DAMON 输出的三个核心指标
DAMON 判断内存访问模式时,最重要的是三个维度。
1. Size:区域大小
表示该监控区域覆盖了多大的地址范围。
单独看到一个区域访问频率很低还不够。如果它只有几个 KB,对系统几乎没有影响;如果它有数百 MB,才可能具备明显的优化价值。
2. Access:访问频率
表示在聚合周期内,该区域的采样页面被访问了多少次,通常会被工具转换为访问比例或者访问频率。
访问频率高,说明它更可能属于业务热工作集。
访问频率低,并不意味着一定可以立即回收,因为它可能只是暂时处于业务低谷。
3. Age:访问模式持续时间
age 很容易被误解为“距离最后一次访问过去了多久”。
更准确地说,它表示当前区域大小和访问频率所形成的访问模式,已经相对稳定地维持了多久。当区域特征发生明显变化时,age 会被重置;特征没有明显变化时,age 会继续增加。(Linux内核文档)
因此,一个更可靠的冷内存候选通常同时具备:
- 区域比较大;
- 访问频率接近零;
age比较高;- 在多个业务周期中都保持相似状态。
可以用下面的方式理解组合结果:
| Size | Access | Age | 可能含义 |
|---|---|---|---|
| 大 | 高 | 高 | 稳定的热工作集 |
| 大 | 低 | 高 | 重点冷内存候选 |
| 大 | 低 | 低 | 可能只是短暂空闲 |
| 小 | 低 | 高 | 虽然冷,但优化收益有限 |
| 大 | 波动 | 低 | 业务阶段正在切换 |
DAMON 是采样系统,所以不能因为某一次快照显示 access=0,就直接认定对应区域永远不会再使用。
六、从 DAMON 到 DAMOS:不只是观察,还能执行策略
DAMON 负责观察访问模式,而 DAMOS 负责把访问模式转换为系统操作。
DAMOS 全称为 Data Access Monitoring-based Operation Schemes。管理员可以声明一组高层策略:
当区域大小、访问频率和年龄满足某些条件时,对这些区域执行指定动作。
DAMOS 支持的代表性动作包括:
stat:只统计,不修改页面状态;willneed:提示内核后续可能需要该区域;cold:把区域标记为冷;pageout:尝试回收对应区域;hugepage:建议使用透明大页;collapse:尝试折叠为大页;lru_prio:提高页面在 LRU 中的优先级;lru_deprio:降低页面在 LRU 中的优先级;migrate_hot:优先迁移较热区域;migrate_cold:优先迁移较冷区域。
具体动作是否可用,取决于所选择的 DAMON 操作集和当前内核能力。(Linux内核文档)
整个决策过程可以概括为:
flowchart LR
A["DAMON访问监控"] --> B["获得Size Access Age"]
B --> C["访问模式规则"]
C --> D["过滤地址或内存组"]
D --> E["检查水位条件"]
E --> F["检查时间与容量配额"]
F --> G{"选择操作"}
G -->|"仅观察"| H["统计Stat"]
G -->|"冷内存"| I["标冷或Pageout"]
G -->|"热内存"| J["提高LRU优先级"]
G -->|"大页候选"| K["HugePage或Collapse"]
这里有三个特别重要的安全机制。
1. Quota:限制动作开销
如果一次找到几 GB 冷内存,直接全部执行 pageout,可能产生大量 CPU、扫描和 IO 开销。
DAMOS 可以限制:
- 每个时间窗口最多执行多长时间;
- 每个时间窗口最多处理多少字节;
- 配额用完后暂停动作;
- 下一时间窗口再重新计算。
这样可以避免优化动作本身制造新的性能问题。(Linux内核文档)
2. Watermark:只在合适的压力区间运行
内存非常充足时,没有必要频繁回收。
内存已经严重抖动时,再进行激进扫描也可能适得其反。
水位机制允许 DAMON 只在特定内存压力区间启动,在压力过低或过高时暂停。
3. Filter:限制操作范围
DAMOS 可以使用地址范围、监控目标、匿名页、活跃页和 memcg 等条件进行过滤。
其中 memcg 能把策略限制在特定内存控制组,为容器和 Kubernetes 场景提供了重要基础。部分过滤能力依赖于具体操作集,不能假设所有内核与模式都支持完全相同的过滤条件。(Linux内核文档)
七、快速体验 DAMON
1. 检查内核能力
DAMON 需要内核在编译时启用相关配置,用户态工具通常通过 sysfs 与 DAMON 交互。官方入门文档要求内核启用相应的 CONFIG_DAMON_* 配置,并确保 sysfs 已挂载。(Linux内核文档)
可以先执行:
uname -r
grep -E 'CONFIG_DAMON|CONFIG_DAMON_SYSFS' \
/boot/config-$(uname -r)
mount | grep ' on /sys '
不同发行版内核启用的 DAMON 功能可能不同。即使存在基础 DAMON 配置,也不代表物理地址监控、回收模块和 LRU 排序模块全部可用。
2. 获取 DAMO 用户态工具
DAMO 是 DAMON 的官方用户态操作工具,可以用于启动监控、读取快照、记录访问模式和生成热力图。(GitHub)
git clone https://github.com/damonitor/damo.git
cd damo
./damo version
sudo ./damo report sysinfo
report sysinfo 可以帮助检查当前内核、DAMON 接口和相关工具支持情况。
3. 监控一个正在运行的 Java 服务
先定位进程:
PID=$(pgrep -n -f 'java.*your-app.jar')
echo "$PID"
启动 DAMON:
sudo ./damo start --target_pid "$PID"
持续查看访问快照:
sudo ./damo report access --repeat
结束后停止监控:
sudo ./damo stop
输出中通常可以看到类似信息:
addr 140.213 TiB
size 64.000 MiB
access 0 %
age 72.300 s
它表达的是:
addr:区域起始地址;size:区域大小;access:聚合周期中的访问强度;age:当前访问模式维持时间。
官方入门示例同样使用 damo start、damo report access 和 damo stop 完成一次进程级快照分析。(Linux内核文档)
4. 记录一段时间并生成热力图
快照适合看当前状态,记录模式更适合观察业务周期。
sudo ./damo record "$PID"
记录结束后生成热力图:
sudo ./damo report heatmap \
--output access-pattern.png \
--draw_range hottest
热力图可以帮助判断:
- 热区是否始终集中在固定地址范围;
- 大量内存是否只在启动阶段被访问;
- 定时任务是否周期性唤醒一批冷区域;
- 工作集是否随着流量增长持续扩张;
- 冷热区域是否频繁切换。
完整记录通常依赖 perf 或 trace-cmd;如果只读取部分 DAMON 快照,则不一定需要安装它们。记录文件还可能包含内存占用、虚拟内存映射和进程统计等辅助数据。(GitHub)
八、在 Java 服务中怎么使用 DAMON
DAMON 看到的是内存页面,而不是 Java 对象。
它无法直接告诉你:
- 哪个 Java 类占用了这块区域;
- 哪一个缓存键长期未使用;
- 哪个对象应该被垃圾回收;
- 某块内存到底来自堆、线程栈还是本地库。
因此,Java 内存问题应该采用分层排查。
| 问题 | 建议工具 |
|---|---|
| Java 堆中是什么对象 | Heap Dump、MAT |
| 对象分配为什么这么多 | JFR、Allocation Profiling |
| JVM 本地内存由什么组成 | Native Memory Tracking |
| 进程映射了哪些地址区域 | /proc/PID/smaps |
| 哪些页面真正频繁访问 | DAMON |
| 系统是否已出现内存停顿 | PSI、vmstat |
| 回收是否造成缺页和 IO | vmstat、sar、perf |
可以先建立基础观测:
cat /proc/pressure/memory
vmstat 1
cat /proc/"$PID"/smaps_rollup
jcmd "$PID" VM.native_memory summary
使用 jcmd VM.native_memory 的前提,是 JVM 启动时已经开启 Native Memory Tracking,例如:
-XX:NativeMemoryTracking=summary
推荐的排查顺序是:
- 用 RSS、PSS 和 NMT 确认内存规模与组成;
- 用 JFR、GC 日志确认对象分配和回收行为;
- 用 PSI、
vmstat判断系统是否正在承受内存压力; - 用 DAMON 查看常驻页面是否属于真实工作集;
- 将 DAMON 冷热变化与接口延迟、定时任务和流量曲线对齐;
- 优先修改缓存、堆大小、对象生命周期和容器限制;
- 最后才考虑自动
pageout或 LRU 调整。
例如,一个 JVM 的 RSS 长期保持在 8 GB,并不能直接说明它泄漏了 8 GB。
但如果 DAMON 显示其中有 3 GB 区域在数十分钟内几乎没有访问,同时应用业务量稳定,那么就值得进一步判断:
- 是否存在过大的历史缓存;
- 是否设置了明显超过工作集的堆;
- 是否有只在启动阶段使用的映射文件;
- 是否存在本地库内存长期常驻;
- 是否可以降低实例内存规格;
- 是否应该调整容器 Request 和 Limit。
九、DAMON_RECLAIM:提前回收冷页面
Linux 还提供了基于 DAMON 的主动回收模块 DAMON_RECLAIM。
它会寻找在指定时间内没有被访问的区域,并尝试回收这些冷页面。它的定位是在轻度内存压力下进行主动、轻量的回收,而不是取代传统的 LRU 页面回收机制。(Linux内核文档)
传统回收往往是压力出现以后才开始工作:
flowchart LR
A["可用内存持续下降"] --> B["kswapd开始扫描"]
B --> C["扫描和回收成本增加"]
C --> D["业务线程申请内存"]
D --> E["可能进入Direct Reclaim"]
E --> F["接口延迟出现尖峰"]
DAMON_RECLAIM 的思路是:
- 在压力尚未失控时识别长期冷区域;
- 在配额约束下逐步回收;
- 尽量减少突发压力下的大规模扫描;
- 降低业务线程进入直接回收的概率。
当前内核文档中的默认冷区域阈值为 120 秒;默认回收时间配额为每个时间窗口 10 毫秒,容量配额为 128 MiB,配额窗口为 1 秒。不同内核版本和发行版可能调整默认值,实际使用时应以本机 sysfs 参数为准。(Linux内核文档)
可以先只读取参数,不要直接修改:
grep . \
/sys/module/damon_reclaim/parameters/enabled \
/sys/module/damon_reclaim/parameters/min_age \
/sys/module/damon_reclaim/parameters/quota_ms \
/sys/module/damon_reclaim/parameters/quota_sz \
/sys/module/damon_reclaim/parameters/quota_reset_interval_ms \
2>/dev/null
如果文件不存在,通常说明当前内核没有编译或加载对应模块。
十、DAMON_LRU_SORT:让冷热页排序更可信
Linux 的页面回收依赖 LRU 体系判断哪些页面应该优先保留、哪些页面可以优先回收。
但在超大内存系统中,如果持续逐页检查访问情况,成本可能很高。因此,LRU 链表通常不是在所有时刻都被完整、主动地重新排序。
DAMON_LRU_SORT 使用 DAMON 识别热区域和冷区域:
- 对热区域提高 LRU 优先级;
- 对冷区域降低 LRU 优先级;
- 通过 CPU 时间配额限制排序成本;
- 通过内存水位决定何时启动或停止。
它不是直接把所有冷页面立即换出,而是在真正发生内存压力前,帮助 LRU 链表更准确地反映页面冷热关系。(Linux内核文档)
相较于直接 pageout,这种方式通常更加温和:
flowchart LR
A["DAMON识别访问模式"] --> B{"页面冷热状态"}
B -->|"热"| C["提高LRU优先级"]
B -->|"冷"| D["降低LRU优先级"]
C --> E["压力来临时优先保留"]
D --> F["压力来临时优先回收"]
十一、在 Kubernetes 中的应用思路
Kubernetes 中的内存问题经常表现为:
- Pod 的
memory.usage长期接近 Limit; - Java 堆没有明显泄漏;
- 节点 PSI 持续升高;
- 容器发生频繁回收甚至 OOM Kill;
- 相同配置的不同实例延迟差异明显。
此时不能只看“Pod 使用了多少内存”,还要判断“这些内存是不是当前工作集”。
一个比较完整的分析链路是:
flowchart LR
A["Pod内存持续升高"] --> B["检查JVM堆和NMT"]
B --> C["检查Cgroup Memory PSI"]
C --> D["DAMON分析页面冷热"]
D --> E{"大量冷内存是否存在"}
E -->|"否"| F["工作集确实过大"]
E -->|"是"| G["排查缓存和内存配置"]
F --> H["扩容或拆分负载"]
G --> I["缩小堆或调整缓存"]
DAMOS 的 memcg 过滤能力可以把部分操作限定在特定内存控制组中,为容器级内存策略提供基础。由于不同内核版本、cgroup 模式和 DAMO 版本的配置方式可能变化,生产中应先确认本机能力,不能直接复制其他环境的参数。(Linux内核文档)
还要特别注意:
如果容器的真实热工作集本身就超过 Limit,那么主动回收不会解决问题,反而可能形成以下循环:
回收热页 → 再次访问 → 发生缺页 → 重新载入 → 再次回收。
这种现象本质上是内存抖动,正确方案应该是增加内存、拆分工作负载或者减少真实工作集,而不是继续提高回收强度。
十二、生产环境应该怎么灰度
第一阶段:只观察,不执行回收
首先使用 DAMON 快照、记录功能或者 DAMOS 的 stat 动作。
至少覆盖以下时间段:
- 业务高峰;
- 业务低峰;
- 定时任务运行期间;
- 大批量数据处理期间;
- 发布后的预热阶段;
- Full GC 前后。
不要只观察几秒钟,就根据单次快照判断冷热。
第二阶段:建立冷内存候选规则
例如,可以先把以下条件作为分析候选,而不是立即执行规则:
- 区域大小超过 16 MiB;
- 访问频率长期接近零;
- 当前模式维持超过 5 分钟;
- 多个观测窗口中持续满足条件;
- 不属于延迟敏感的关键映射;
- 业务正处于正常流量阶段。
这些阈值没有统一答案,需要根据业务访问周期进行调整。
一天运行一次的任务,即使 23 小时都显示为冷,也不代表对应数据可以毫无代价地回收。
第三阶段:单实例、小配额测试
选择一台非核心实例进行灰度,并限制:
- 每个窗口处理的最大内存;
- 每个窗口允许消耗的 CPU 时间;
- 策略生效的内存压力区间;
- 可操作的进程或内存控制组;
- 冷区域的最小年龄。
同时观察:
cat /proc/pressure/memory
vmstat 1
grep -E 'pgfault|pgmajfault|pgscan|pgsteal|pswpin|pswpout' \
/proc/vmstat
业务侧还要同步观察:
- P50、P95、P99 延迟;
- 请求吞吐量;
- 错误率;
- CPU 使用率;
- 磁盘读写量;
- Major Page Fault;
- Swap In 和 Swap Out;
- 实例重启与 OOM 次数。
第四阶段:比较净收益
不能只看“RSS 降了多少”。
真正需要比较的是:
释放的内存价值,是否大于重新访问这些页面造成的缺页、IO 和延迟成本。
一个策略即使释放了 2 GB 内存,但导致 P99 延迟从 100 毫秒升到 500 毫秒,也不能称为有效优化。
DAMOS 还提供 sz_tried、sz_applied 和 qt_exceeds 等统计信息,可用于判断有多少区域进入候选、多少区域实际执行了动作,以及配额被触发了多少次。
十三、使用 DAMON 最容易踩的坑
1. 把 RSS 当成真实工作集
RSS 只代表页面当前驻留在物理内存中,不代表这些页面正在被业务频繁使用。
2. 把一次 access=0 当成永久冷内存
DAMON 是采样监控。短时间没有采样到访问,不代表区域永远不会被访问。
3. 把 age 简单理解为最后访问时间
age 表示当前访问模式的持续时间,需要结合访问频率一起判断。
4. 一上来就启用 pageout
正确顺序应该是:
快照观察 → 长时间记录 → 识别候选 → 统计模式 → 小配额灰度 → 自动执行。
5. 采样间隔设置得过小
采样越频繁,理论精度越高,但监控开销也会增加。
官方设计文档建议根据业务访问强度选择聚合周期,并让采样周期与聚合周期保持合理比例。默认的聚合周期在部分大型系统上可能过短,参数应根据实际结果调整。
6. 只看内存下降,不看 Major Fault
冷页被回收后,如果很快再次访问,就可能产生大量 Major Page Fault 和磁盘读取。
这通常意味着冷内存阈值过于激进,或者识别出的并不是真正冷页。
7. 忽视数据库和缓存的预热成本
数据库 Buffer Pool、搜索索引和本地缓存即使一段时间没有访问,也可能在流量恢复后迅速变热。
回收这些页面虽然能暂时降低 RSS,却可能让下一次流量高峰承担重新加载成本。
8. 同时启用多个页面访问跟踪机制
DAMON 的部分操作集通过 PTE Accessed Bit 检查访问情况。官方文档指出,它可能与 Idle Page Tracking 等同样依赖访问位的机制产生干扰,因此同时使用前应进行专门评估。
十四、结语
过去我们做内存优化,关注的通常是一个数字:
这个进程占了多少内存?
而 DAMON 带来的变化,是把问题继续向前推进一步:
这些内存现在到底有没有被使用?
一旦能够看到内存访问频率和冷热变化,很多问题都会变得更加清晰:
- RSS 高,到底是泄漏还是合理缓存;
- JVM 堆大,到底是工作集需要还是配置过度;
- 系统抖动,到底是 CPU 不足还是内存回收;
- 容器接近 Limit,到底应该扩容还是缩小无效常驻内存;
- 冷页面应该直接回收,还是只降低 LRU 优先级。
DAMON 并不是一个“一键释放内存”的工具。
它真正的价值,是把内存管理从静态容量统计,推进到动态访问模式分析。生产环境也不应该从 pageout 开始,而应该从观测开始:先知道哪些内存真正热、哪些内存长期冷,再决定是否调整缓存、堆大小、容器规格和回收策略。
当系统开始从“用了多少内存”转向“这些内存有没有价值”,内存优化才真正进入精细化阶段。

评论区 0