Linux 内存文件与磁盘排障:RSS、nofile 与 I/O 账单
结论基线:内存与限制语义依据 proc_pid_status(5)、getrlimit(2)、内核 cgroup v2 文档(查询 2026-10-25),JVM 容器内存感知固定 OpenJDK
jdk-17.0.12+7源码摘录;本机实测 macOS 26.7.1 + Oracle JDK 17.0.12+8(mem/fd/disk模式)。Linux 专属记账(/proc/meminfo、swap、io.stat、page cache 写回、OOM killer)按文档推导并逐处标注未实跑;NMT/堆证据的 JVM 权威在 JVM 诊断正文,cgroup 记账权威在 cgroup 正文,本页不重复。
问题:内存与磁盘的账单在哪儿对不上
"堆只用了 4G,容器 8G 却被 OOMKilled"、"free 还剩很多,进程却 too many open files"、"删除了日志磁盘还是满的"——三句话是同一类错误:把不同层的账单相加或相减。JVM 账单(NMT/committed)、内核账单(RSS/page cache/swap)、cgroup 账单(memory.current)与文件系统账单(df/inode)各自独立成立。本页给对账边界与恢复顺序;网络 FD(TIME_WAIT/CLOSE_WAIT)不在此展开,见cgroup 正文 FD 节与 TCP 正文。
进程内存:VSZ、RSS 与对账边界
三个量先分开:VSZ 是虚拟地址空间(预留,含未触页);RSS 是驻留物理页(含共享库与页缓存映射,proc_pid_status(5) 拆成 RssAnon/RssFile/RssShmem 与 VmSwap);committed 是 JVM 已提交的内存承诺。三者没有恒等式:committed > RSS(提交未触页)、RSS > 堆使用(native+元空间+GC 结构)、共享页让两个进程的 RSS 之和虚高。
本机实测(macOS 26.7.1,Oracle JDK 17.0.12+8,mem 模式):-Xms64m -Xmx512m,业务保留 400×256KB(约 100MB):heapUsed=133MB、RSS 31MB→175MB、NMT total committed=339MB(其中 Java heap committed=232MB)。三条读数各自成立——RSS(175MB) > heapUsed(133MB)(红6:RSS≈堆被推翻),RSS(175MB) < heap committed(232MB)(提交不等于驻留),NMT(339MB) 是 JVM 自记账不含全部映射。工程含义:给容器配内存用 RSS/memory.current 做上界观测,用 NMT 差量找 native 涨幅(NMT 正文),不要拿堆配置推断容器内存需求。
堆内分配也受对象形态约束(本票实测红3):-Xmx512m 装 300×1MiB 数组在 G1 下 OOM——1MiB 是 humongous 对象(≥Region 一半),各需 2 个连续 Region,300 个要 ~600MiB 连续 Region;改 256KB 小块 400 个即正常。G1 机制权威在 GC 选型正文;本页把它当"OS 内存够、堆容量也'够'、仍 OOM"的反例。swap:si/so 持续非零说明匿名页在换入换出,GC 停顿与尾延迟会跟着抖(Linux 观测推导,未实跑;cgroup 侧 memory.swap.max 到顶后匿名页不再换出,见 cgroup v2 文档)。
文件描述符与 nofile:泄漏的定位与恢复
FD 是进程级的打开文件句柄预算:socket、文件、管道、eventfd 共用同一张表。getrlimit(2):RLIMIT_NOFILE 是"最大可打开描述符号+1",超限的 open/pipe/dup 报 EMFILE;软限制可调、硬限制是上限,/proc/PID/limits 给出该进程实际生效值(查询 2026-10-25)。与 cgroup 正文分界:那边讲"数量之外找持有者"(连接/半关闭),本页讲限制本身与泄漏恢复。
本机实测(macOS + Oracle JDK 17.0.12+8,fd 模式,子进程 RLIMIT_NOFILE=80):
- 泄漏相位:循环打开文件不关,第 77 次打开(成功 76 次)抛
Too many open files——阈值=限制−JVM 自身占用(80−4)。红4 另证:64 太低会让 JVM 启动即失败(GraalVM JVMCI dlopen EMFILE),nofile 余量是 JVM 启动前提。 - GC 不回收:持有 76 个
FileInputStream调System.gc()后重试 20 次全部 EMFILE(GC_RETRY_OPENED=0,红8)——FD 不是堆对象,finalize 时代也不承诺及时回收。 - 恢复:try-with-resources 逐个关,200/200 成功。
恢复顺序:降低占用(关流/收窄池)→ 复验阈值以下稳定 → 必要时调软限制(ulimit -n/systemd LimitNOFILE,属部署清单)→ 监控 /proc/PID/fd 数量与类型趋势;把限额调高当修复会掩盖泄漏到下一层(磁盘/内核内存)。
deleted-but-open:删除路径不等于释放空间——fd 还开着,数据块被进程持有。本机实测(disk 模式):写 64MB、delete 后 usable 空间不变(deletedWhileOpen == withFile == 250305MB),通道仍可读出数据(STILL_READABLE=true);关闭后 macOS/APFS 因异步记账在 1.2s 内未立刻反映(释放时点是观察值不是契约,Linux close 后 df 回收同样依赖记账与挂载点)。"删了日志磁盘还满"先查 lsof +L1//proc/*/fd 指向已删除文件的大持有者,让进程关闭或滚动重启,再谈扩容。
磁盘 I/O:观测、D 状态与受限资源
观测分层:应用侧 fsync/写时延(本机 disk 模式实测 64KB write+force 每次 21–23ms,单机观察值);工具侧 iostat 的 KB/t、tps、MB/s(本机实测正常输出);Linux 侧补充 await、util% 与 cgroup io.stat(rbytes/rios/wbytes/wios/dbytes/dios)与 io.max(rbps/wbps/riops/wiops 限流器,超限短暂突发允许——cgroup v2 文档口径,查询 2026-10-25,未实跑)。
三条因果链分开排:fsync 慢看设备/卷类型与并发写放大;读慢优先怀疑页缓存未命中(free 的 buff/cache,Linux 推导);不可中断是 D 状态——ps 里 D 的线程在等内核 I/O,kill -9 无效(信号要等系统调用返回才处理),恢复责任在存储/内核层(NFS 掉线、坏盘、节流的 io.max)。RLIMIT_FSIZE(getrlimit(2))是另一类失败:写超限触发 SIGXFSZ、默认终止进程。容量对账:df 看块、df -i 看 inode(小文件海耗尽 inode 时 df 有余量仍 ENOSPC,Linux 推导未实跑);NIO 背压正文处理写不动的上游对策。
容器内存感知:JVM 读取限制但不替内核执行
JVM 的"可用内存"认知在容器里来自 cgroup:
原码连续摘录(仅统一公共缩进,截取函数局部):os_linux.cpp,jdk-17.0.12+7,L189–L215。
julong os::available_memory() {
return Linux::available_memory();
}
julong os::Linux::available_memory() {
// values in struct sysinfo are "unsigned long"
struct sysinfo si;
julong avail_mem;
if (OSContainer::is_containerized()) {
jlong mem_limit = OSContainer::memory_limit_in_bytes();
jlong mem_usage;
if (mem_limit > 0 && (mem_usage = OSContainer::memory_usage_in_bytes()) < 1) {
log_debug(os, container)("container memory usage failed: " JLONG_FORMAT ", using host value", mem_usage);
}
if (mem_limit > 0 && mem_usage > 0) {
avail_mem = mem_limit > mem_usage ? (julong)mem_limit - (julong)mem_usage : 0;
log_trace(os)("available container memory: " JULONG_FORMAT, avail_mem);
return avail_mem;
}
}
sysinfo(&si);
avail_mem = (julong)si.freeram * si.mem_unit;
log_trace(os)("available memory: " JULONG_FORMAT, avail_mem);
return avail_mem;
}容器化时 avail = memory_limit − memory_usage(限额与用量都取不到才回退宿主 sysinfo);物理内存认知同理取 memory_limit_in_bytes():
原码连续摘录(仅统一公共缩进,截取函数局部):os_linux.cpp,jdk-17.0.12+7,L217–L230。
julong os::physical_memory() {
jlong phys_mem = 0;
if (OSContainer::is_containerized()) {
jlong mem_limit;
if ((mem_limit = OSContainer::memory_limit_in_bytes()) > 0) {
log_trace(os)("total container memory: " JLONG_FORMAT, mem_limit);
return mem_limit;
}
}
phys_mem = Linux::physical_memory();
log_trace(os)("total system memory: " JLONG_FORMAT, phys_mem);
return phys_mem;
}这条链由 JDK-8146115(Fixed in JDK 10 b34,backport 8u191/192/201/202,查询 2026-10-25)引入:memory_limit 初始化 os::physical_memory(),-XX:+UseContainerSupport 默认开(java(1) 手册口径:容器支持默认启用,-Xlog:os+container=trace 可观察判定过程)。MaxRAMPercentage 按这个"容器物理内存"切堆:2GiB 限额 × 50% → 堆上限 ~1GiB。感知不等于约束:JVM 读的是限额,OOM killer 执行的也是限额——但 1.5GiB 堆 + 512MiB"非堆余量"的算术(cgroup 正文)说明感知帮不了"堆配置本来就装不下"的部署;MaxRAMPercentage 过高时 GC 余量与 native(栈×线程数、元空间、直接缓冲)把 RSS 顶到 memory.max,内核杀进程(OOMKilled,137)而不是给 Java 抛 OOME——那条失败路径没有 hook 与 dump(信号正文)。macOS 无 cgroup:本页两段摘录在 macOS 上不可复现,mem 模式只实测 RSS/NMT 对账。
实验:内存、文件与磁盘观测
公开实验入口:mem 模式(RSS/heapUsed/NMT 三方对账 + 分配前后 RSS 差量断言)、fd 模式(nofile=80 下 EMFILE 阈值、GC 不回收、关闭恢复三相位)、disk 模式(deleted-but-open 与 fsync 观察值 + iostat 采样)。thread/perm/signal 模式见进程线程页与信号权限页。所有内存/时延数值为单机观察值;cgroup io/memory 控制器行为未实跑。
架构师推演:内存与磁盘的容量余量(算例,非实测)
设容器限额 4GiB:堆 2GiB(50%)、元空间+代码缓存 256MiB、GC/线程栈 300 线程×1MiB≈300MiB、直接缓冲 512MiB、页缓存与第三方 native 余量 512MiB——合计 ≈3.6GiB,剩 0.4GiB 是峰值与诊断余量(dump/日志),不是可分配的新容量。再放第二个同规格 JVM 进同一限额,每份预算减半——"感知"读到的 limit−usage 会实时反映,但堆上限不随之收缩,OOMKilled 只会挪到更晚。磁盘侧:写放大 3×(数据+双写+日志)时,按峰值吞吐预留 IOPS 与带宽要用 io.stat 实测分布而非标称值;fsync 预算进入事务路径(存储事务正文的持久化成本),恢复责任按层分:限额是部署清单、泄漏是应用修复、设备慢是存储层工单。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
- proc_pid_status(5)(VmRSS/RssAnon/RssFile/VmSwap)、getrlimit(2)(RLIMIT_NOFILE/RLIMIT_FSIZE)、proc_pid_limits(5)(查询 2026-10-25)。
- 内核 cgroup v2 文档 memory/io 控制器与 memory.swap.*(查询 2026-10-25);JDK-8146115(Fixed 10 b34,backport 8u191 等,查询 2026-10-25);JDK 17 java(1) 手册 UseContainerSupport/-Xlog:os+container=trace(查询 2026-10-25)。
- OpenJDK
jdk-17.0.12+7os_linux.cppL189–L215、L217–L230(本页摘录);运行 JDK Oracle 17.0.12+8 构建号不同分记。 - 本机实验:labs/os-network mem/fd/disk 模式与证据记录;Linux meminfo/swap/io.stat/inode/OOM killer 未实跑,全部标注推导。