尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Linux内存管理实战:虚拟内存、物理内存与进程地址空间排查

发布时间:2026/9/29 7:26:34

资讯中心
01
ARTICLE

Linux内存管理实战:虚拟内存、物理内存与进程地址空间排查

Linux内存管理实战:虚拟内存、物理内存与进程地址空间排查
1. 先把三个词的边界划清楚虚拟内存、物理内存与进程地址空间线上告警一响很多人第一反应是敲 top看到 used 高就慌看到 free 少就想加内存条。这套反应我早年也走过后来被一位老运维按住手先把 VSZ、RSS、available 三个数读明白再决定要不要动刀。 Linux 内存管理这件事绕不开虚拟内存、物理内存、进程地址空间这三个概念——它们是整块拼图的骨架也是面试和实战里被问得最多、错得最多的部分。虚拟内存给进程发的是提货凭证物理内存才是真正堆货的仓库而进程地址空间就是每本凭证上标注的取货范围。三者的配合一旦理解错你看到的每一个数字都会骗你。这篇内容偏向实战拆解适合三类人刚入行不久、被free输出绕晕的运维新人写 C/C、Go、Java 服务想搞清楚自己进程到底吃多少内存的后端开发以及准备系统方向面试、需要把碎片知识串成一条线的同学。我不会一上来就甩源码结构体而是先讲清楚为什么要这么设计再落到可以亲手敲的命令和参数最后把踩过的坑摊开说。1.1 用一个借书证类比三者的分工想象一座城市的图书馆。每一本书有固定的架位号这是物理地址但图书馆不会让读者直接冲进书库乱翻而是给每人发一张借书证证上写的是第 3 排第 5 层左手第二本这样的虚拟地址。读者拿着证去前台前台查一张对照表把编号翻译成真实架位再把书递出来。这张对照表就是页表负责翻译的前台就是MMU内存管理单元集成在 CPU 里而借书证上的那个可用编号范围就是进程地址空间。这个类比里最关键的一点是每位读者的借书证编号都从 1 开始互相看不见对方。进程 A 眼里的 0x400000 和进程 B 眼里的 0x400000完全是两个不同的物理位置。地址隔离这件事带来的收益远超成本——没有它一个程序写越界就能改坏另一个程序的数据操作系统就得回到单道批处理的年代。另一个隐性收益是稀疏使用你申请 1GB 内存内核立刻给你 1GB 虚拟地址区间但物理页要等你真正写到那一页才分配这叫按需分页demand paging。很多程序申请了 1GB实际只用了 100MB靠的就是这套机制活着。1.2 用户态、内核态、硬件三层各管一段Linux 内存管理的代码横跨三个层次理清分层之后看源码才不迷路。最上层是用户态接口malloc、mmap、brk、shmget这些属于 libc 和系统调用层进程能直接感知中间层是内核的内存管理子系统它负责维护进程地址空间mm_struct、vm_area_struct、管理物理页框struct page、做页表映射和缺页处理mm/目录下那几万行代码全在这里最下层是硬件包括 MMU、TLB快表、各级缓存它们按照页表给出的规则默默工作内核只能通过 TLB 刷新指令如invlpg和缓存控制指令去影响它。理解这个分层带来的直接好处是以后看到内存没释放你先判断问题出在哪一层。用户态malloc没调free那是应用层泄漏free调了但 RSS 不降可能是 glibc 没把内存还给内核arena 缓存内核层 slab 缓存持续增长得用slabtop去看具体哪个缓存项在膨胀再往下如果是页表本身占得太多那就是映射数量爆炸vm.max_map_count相关。不同的层有不同的观察工具用错工具等于白忙一场。2. 进程地址空间是怎么被画出来的从一次 malloc 说起一个 C 程序启动之后内核给它准备了一整套虚拟地址布局代码段、数据段、BSS、堆、栈、共享库映射区、内核映射区每一块都有明确的范围和权限。这套布局不是拍脑袋定的而是几十年演进下来的折中结果——既要给共享库留出足够的随机化空间ASLR又要让brk扩展堆的路径尽量短还得给栈留出向低地址生长的余量。想搞懂valgrind、pmap、/proc/PID/maps输出的那些行到底代表什么先得把这张布局图刻在脑子里。2.1 32 位与 64 位的地址空间布局差在哪32 位时代最经典的划分是03GB 给用户34GB 给内核也就是 1:3 的分割比例。这个比例在 4GB 物理内存的机器上刚刚好内存一涨到 8GB、16GB内核就不得不用HIGHMEM高端内存来做临时映射才能访问到全部物理内存——因为内核能直接线性映射的虚拟地址只有 1GB。这是 32 位系统的历史包袱也是为什么很多老服务在 32 位环境下会出现明明内存够用却分配失败的怪现象。x86-64 下事情宽松多了。当前主流是四级页表、48 位虚拟地址用户空间和内核空间各占 128TB0x0000_0000_0000_0000到0x0000_7fff_ffff_ffff是用户态高地址一半是内核态。128TB 什么概念你可以在一台 16GB 物理内存的机器上让一个进程映射 100TB 的虚拟内存而毫无压力——只要不去写它。这也是容器和 JVM 敢把虚拟地址空间规划得那么大的底气所在。近几年 5 级页表LA57开始在新硬件上铺开虚拟地址扩大到 57 位用户空间涨到 64PB但那是给超大规模内存场景准备的日常碰不到。需要注意的是 ASLR地址空间布局随机化。同一份代码跑两次栈基址、共享库基址、堆基址都会不一样这是内核故意加的安全特性。调试时如果发现某次崩溃地址和上次对不上别急着怀疑人生先cat /proc/sys/kernel/randomize_va_space看一眼是不是 2完全随机化。临时定位问题时可以设为 0 复现但生产环境千万别关。2.2 mm_struct 与 vm_area_struct内核眼里的进程内存地图内核怎么描述一个进程的内存答案是一主多从的数据结构。mm_struct是总账本每个进程一个线程之间共享里面记录着整个地址空间的起止范围、各段起始地址start_code、end_code、start_data、start_brk、brk、start_stack、页表根指针pgd、映射区域的红黑树根mm_rb、VMA 链表头mm_mmap以及驻留集统计rss_stat。vm_area_struct简称 VMA是分账明细每一段连续、权限相同的虚拟区间对应一个 VMA记录vm_start、vm_end、vm_flags读/写/执行/共享、vm_file如果是文件映射和vm_ops操作函数集。一个普通进程有多少个 VMA空壳程序大概十几个一个跑着 JVM 或者带了几十个.so的服务几百到几千个都正常。/proc/PID/maps里每一行就是一个 VMA/proc/PID/maps的行数直接等于 VMA 数量。为什么要用红黑树加链表两套结构因为查找方式不同——按地址查找 VMA缺页时用走红黑树O(log n)顺序遍历全部映射fork时复制、munmap时检查走链表。内核里这种一套数据、两种索引的设计非常常见看到类似结构别觉得冗余先想清楚两个访问路径分别是谁在用。VMA 还有一个容易被忽略的作用它承载了分配语义。当进程调用malloc(1MB)时glibc 通常走mmap内核创建一个新的匿名 VMA权限是读写、不关联文件而malloc(16)走的是堆扩展改写的是brk指针VMA 只是被拉伸或合并。这就是为什么同一份程序里不同大小的分配在/proc/PID/maps里表现完全不同。2.3 malloc 到底是走 brk 还是 mmap这是被问得最多的问题之一答案是有阈值。glibc 默认的M_MMAP_THRESHOLD是128KB小于它的分配从堆上切brk区域大于等于它的走mmap单独映射一个匿名段。这个阈值不是固定的glibc 有动态调整机制当你free掉一块大于当前阈值的 mmap 内存时阈值会被抬高到那块内存的大小上限 32MB64 位。这个动态机制是为了避免大块分配、释放、再分配反复触发 mmap/munmap 系统调用。分配方式典型触发条件释放行为常见观察现象brk小块 128KB不一定归还内核可能留在 arenaRSS 不降VSZ 稳定mmap匿名映射大块≥ 128KBfree后通常直接munmapRSS 立即下降mmap文件映射mmap()显式调用依赖引用计数与msync计入Mapped字段实操上有个细节值得记查看阈值可以用MALLOC_MMAP_THRESHOLD_环境变量覆盖也可以用mallopt(M_MMAP_THRESHOLD, size)在代码里调。我处理过一个日志服务的 RSS 缓慢增长问题最后定位到就是频繁分配 100KB200KB 的缓冲区正好卡在阈值边缘来回震荡导致 arena 里堆了一片无法回收的碎片。把缓冲区改成固定大小的对象池之后RSS 曲线立刻拉平。提示glibc的多线程分配有一个常被忽略的设定——arena 数量默认是8 × CPU 核数64 位下。每个线程第一次分配内存时可能绑定到一个新 arena而单个 arena 在 64 位下的虚拟地址上限是 64MB。线程多、分配频繁的服务光 arena 的虚拟地址占用就可能上千 MB。设MALLOC_ARENA_MAX2~4往往能显著压低 VSZ 和碎片代价是极端并发下锁竞争会变强。3. 虚拟地址到物理地址的那一跳页表、MMU 与缺页异常前面说的都是账本现在看翻译这一步。CPU 执行指令时拿到的是虚拟地址必须经过 MMU 翻译成物理地址才能访问内存。翻译的依据是页表翻译的结果被缓存进 TLB。翻译过程中如果发现缺少映射或者权限不符硬件会抛出一个缺页异常page fault把控制权交给内核的do_page_fault由内核决定是补一页、换一页还是直接给进程发 SIGSEGV。这一步是整个内存管理里唯一每次访存都要走的路径它的效率直接决定程序性能。3.1 多级页表与 TLB为什么不做一本大字典最朴素的想法是一张大表把 48 位虚拟地址全映射一遍。算一下就知道不行——48 位地址空间、4KB 页大小需要 2^36 个页表项每项 8 字节合计 512GB。每个进程一张内存直接爆掉。真实做法是多级页表x86-64 四级页表把 48 位虚拟地址切成PGD(9) PUD(9) PMD(9) PTE(9) 页内偏移(12)每一级 512 项。关键在于按需分配进程只用了几百 MB 内存那么绝大部分 PGD 表项是空的下面的 PUD/PMD/PTE 表根本不存在。稀疏结构换空间这是多级页表存在的唯一理由。代价是访存次数。一次翻译理论上要走 4 次内存读取每级页表一次加上真正取数据那次一次访存变五次性能直接腰斩。所以 CPU 里塞了 TLB——一个几十到几百项的高速缓存专门存最近用过的虚拟页到物理页的映射。TLB 命中时翻译是零开销的。TLB 有多大典型的数据 TLB 一级缓存 64 项左右二级 15002000 项。这组数字解释了一个现象频繁访问大范围、随机分布的地址TLB 命中率会崩性能随之骤降。这也是大页HugePage能提速的根本原因——一个 2MB 大页只用一项 TLB 覆盖 512 倍于 4KB 页的范围。3.2 缺页异常的三种典型面孔缺页异常不是错误是正常流程。它至少有三种形态用ps -o maj_flt,min_flt或者/proc/PID/stat的对应字段能看到计数。类型触发条件内核动作代价次缺页minor fault页已在内存只是当前进程没有映射建立页表项建立反向映射微秒级主缺页major fault页不在内存需从磁盘/swap 读入发起 I/O阻塞等待毫秒级慢 1000 倍非法访问地址未映射或权限不符发送 SIGSEGV / SIGBUS进程终止次缺页最常见的场景是fork之后子进程第一次写内存以及匿名内存首次被写。主缺页则和文件读取、swap 换入强相关。判断一个服务的性能瓶颈是不是内存导致看一眼主缺页率就能筛掉一大半可能性——如果maj_flt每秒几百上千次说明系统正在反复从磁盘捞数据内存明显不够用了。注意maj_flt计数是累计值别直接看绝对值。正确做法是间隔采样算差值cat /proc/PID/stat | awk {print $12, $10}取两次除上间隔秒数得到每秒速率。这个坑我见太多人踩了。3.3 fork 为什么快写时复制的账怎么算fork一个占 2GB 内存的进程理论上要复制 2GB 数据实际上几毫秒就返回了。原因就是写时复制Copy-On-WriteCOW。fork时内核并不复制物理页只复制页表——父子的页表项都指向同一批物理页同时把这些页标记为只读。谁先写谁触发一次缺页内核此时才真正复制一页出来改好页表、恢复可写。这套机制带来的第一个后果是父子进程共用未修改的内存实际物理占用远小于 2×2GB。第二个后果更微妙——fork之后立刻exec启动新程序那么 COW 复制的页几乎全被丢弃等于白做。所以现代启动子进程的场景更推荐posix_spawn或者vfork能绕开这一层。COW 还有一个隐形成本容易被低估页表本身的复制。大内存进程的页表可能有几十 MBfork时这部分是要实打实复制的。所以一个进程如果映射了 100GB 的稀疏地址空间比如 JVM 堆预留fork出来的子进程光是页表拷贝就要花掉几十毫秒。高频fork的场景比如老式 CGI、某些 PHP-FPM 配置性能差很多时候根子在这里。4. 物理内存怎么发出去伙伴系统、slab 与页缓存虚拟地址讲完了该看真实的物理页怎么管。Linux 的物理内存管理是分层的从上往下依次是 NUMA 节点、内存区域zone、页框page分配时从伙伴系统拿整页小对象则走 slab 分配器从页里切。除此之外还有一大块内存被页缓存占用它既不是某个进程的私有财产也不能简单地算作已用。绝大多数关于内存的误判都发生在没搞清楚这个数字属于哪一层上。4.1 node、zone、page物理内存的三层组织最顶层是节点node对应 NUMA 架构里的一个 CPU 内存控制器。单路机器通常只有一个 node双路服务器有两个。每个节点用pglist_data描述节点内再划分 zone。zone 的划分是历史遗留和硬件限制的产物ZONE_DMA给那些只能做 24 位寻址的老式外设用通常在 16MB 以下现代系统基本空着。ZONE_DMA32只能做 32 位寻址的设备用x86-64 上常见。ZONE_NORMAL常规内存绝大多数内核分配都从这里来。ZONE_HIGHMEM32 位时代的高端内存64 位系统上不存在。ZONE_MOVABLE为内存热插拔和减少碎片预留的可迁移区。每个 zone 内部用free_area[MAX_ORDER]数组管理空闲页MAX_ORDER默认是 11对应最大 4MB 的连续块2^10 × 4KB。每个物理页有一个struct page描述符典型的 64 字节大小。算一下16GB 内存、4KB 页共 400 万个页光是struct page数组就要吃掉 256MB。这就是为什么内核会选择用vmemmap把struct page数组映射成一个紧凑的虚拟连续区域——节省的是 TLB 和寻址开销。4.2 伙伴系统与内存碎片伙伴系统的核心思想是按 2 的幂次分配。你要 3 个页它给你 4 个页的块你要 5 个页它给你 8 个页。释放时如果伙伴块也空闲就合并成更大的块向上递归。这保证了分配和释放都是 O(log n)也保证了总能找到尽可能大的连续块。问题是长期运行后不可避免的外部碎片。空闲页总数可能还有 1GB但没有一块连续的 4MB需要大块连续内存的分配比如某些 DMA 缓冲区、大页申请就会失败。内核提供了两个应对手段内存压缩compaction把可迁移页挪走腾出连续区域以及ZONE_MOVABLE机制把可迁移和不可迁移的页分开放。看碎片情况直接用/proc/buddyinfo$ cat /proc/buddyinfo Node 0, zone DMA 1 1 1 0 2 1 1 0 1 1 3 Node 0, zone DMA32 1234 1088 823 512 301 188 102 61 30 11 4 Node 0, zone Normal 30012 20114 12003 6102 3011 1502 701 312 140 55 12每一列表示该 order 下的空闲块数量order 0 是 4KB 单页order 10 是 4MB 块。如果左边几列数字很大、右边几列接近 0说明系统碎片严重遇到需要大块连续内存的分配就可能失败即使总空闲量看起来还很充裕。这个现象在老机器上的典型表现是fork或者mmap大页时报 ENOMEM。4.3 slab/slub 与 kmalloc小对象怎么省着花内核自己要分配的对象大多很小task_struct、dentry、inode、网络套接字缓冲区都是几百字节。如果每个都占一整页 4KB浪费太夸张。slab 分配器就是为了切页而生它从伙伴系统拿整页按对象大小切成等份用空闲链表管理。同一个缓存里的对象类型相同初始化一次可以反复复用。Linux 历史上有三种实现slab、slub、slob。现在主流是slub代码更简洁、调试友好、在大型系统上扩展性更好。查看方式$ sudo slabtop -o -s c | head -15 Active / Total Objects (% used) : 2891234 / 3120987 (92.6%) Active / Total Slabs (% used) : 98234 / 98234 (100.0%) Active / Total Caches (% used) : 118 / 165 (71.5%) Active / Total Size (% used) : 512345.67K / 561234.12K (91.3%) Minimum / Average / Maximum Object : 0.01K / 0.29K / 8.00K OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME 892500 882301 98% 0.19K 42500 21 3400000K dentry 621400 601200 96% 1.05K 31070 20 2485600K ext4_inode_cache ...dentry和inode缓存排在前两名是极其正常的现象它们会随着文件访问自然增长也会在内存压力下被回收——注意SReclaimable和SUnreclaim的区别前者能还后者不能还。如果某天发现SUnreclaim持续涨而不落那才是真问题一般是某个内核模块的缓存泄漏。实操心得kmalloc和vmalloc的区别值得记住。kmalloc从线性映射区分配物理连续速度快但大小受限一般不超过 4MBvmalloc只保证虚拟连续物理上可以不连续代价是多一层页表和多一次 TLB 未命中。写驱动或者看内核日志时看到vmalloc分配失败但内存还有富余八成是虚拟地址空间不够x86-64 上 32TB 左右得查/proc/vmallocinfo。4.4 页缓存、脏页回写与回收水位文件读写不会直接落到磁盘中间隔着页缓存page cache。读的时候内核先把文件页读进内存下次读同一块直接命中内存写的时候先写进内存里对应的页标记为脏页dirty再由pdflush/writeback内核线程批量刷回磁盘。这就是为什么free输出里buff/cache常年占大头也是为什么很多内存泄漏的错觉其实来自页缓存。脏页刷盘的策略由两组 sysctl 控制参数默认值含义vm.dirty_background_ratio10脏页占内存比例超过此值后台线程开始异步回写vm.dirty_ratio20超过此值写操作同步阻塞直到脏页降下来vm.dirty_expire_centisecs3000脏页最长驻留时间30 秒vm.dirty_writeback_centisecs500回写线程唤醒间隔5 秒这两个 ratio 参数是很多服务周期性卡顿的真凶。如果你的服务在大文件写入时每几十秒出现一次几百毫秒的停顿大概率就是脏页冲到dirty_ratio触发了同步写回所有写入线程一起被阻塞。把dirty_ratio调低比如 5、dirty_background_ratio调更低比如 2让回写更平滑地发生往往能明显改善延迟毛刺。代价是磁盘 IO 更频繁SSD 上这点代价可以忽略。回收这部分还涉及水位线机制。每个 zone 有三个水位WMARK_MIN、WMARK_LOW、WMARK_HIGH由vm.min_free_kbytes决定基准。空闲页降到 LOW 以下唤醒kswapd后台回收降到 MIN 以下任何分配请求都要先自己做一次直接回收direct reclaim这时分配线程会被拖住表现为分配内存变慢。这就是内存吃紧时服务响应变慢的底层原因而不是 CPU 不够。5. 动手排查命令和内核接口怎么用才不误判理论说完了进入实操。这一节按看全局 → 看进程 → 看细节的顺序来每条命令都配上怎么读、容易误读在哪。我自己的习惯是先看free定大方向再用smaps_rollup定具体进程最后用smaps或采样对比找具体区域。5.1 free、top、ps、pmap 的正确读法先看free -h的输出$ free -h total used free shared buff/cache available Mem: 62Gi 28Gi 3.2Gi 1.1Gi 31Gi 32Gi Swap: 8.0Gi 512Mi 7.5Giavailable才是还能给新程序用多少内存的答案不是free。free只有 3.2GB 看着吓人但available有 32GB因为那 31GB 的buff/cache大部分是可回收的页缓存。这个字段是老版本free没有的procps 3.3.10 之后才有现在还在用老版本系统的同学建议顺手升级一下工具包能少踩很多坑。再看进程级。ps aux里的VSZ和RSS是新手最容易搞混的两个数指标全称含义用途VSZ / VIRTVirtual Set Size整个虚拟地址空间大小判断映射规模不能当内存占用量RSS / RESResident Set Size已驻留在物理内存的页大小粗看实际占用但共享页会重复计算PSSProportional Set Size共享页按共享进程数均摊容器/多进程场景更公平USSUnique Set Size进程独占的物理内存判断进程真实私有开销RSS的坑在于100 个进程映射了同一个 200MB 的.so这 200MB 会在每个进程的 RSS 里各算一次加起来变成 20GB实际物理上只有一份。容器内存限制打不准、账单算不清很多时候就栽在这里。想知道真实的均摊值得看PSS$ cat /proc/1234/smaps_rollup Rss: 5123456 kB Pss: 1023456 kB Pss_Anon: 800000 kB Pss_File: 223456 kB Pss_Shmem: 0 kB Shared_Clean: 4000000 kB Private_Dirty: 800000 kB Anonymous: 800000 kB Swap: 0 kBsmaps_rollup是内核 4.14 引入的把原来要遍历整个smaps的统计一次算好代价极低可以放心放进监控脚本里定期采集。老的smaps文件在大内存进程上跑一次要几秒甚至几十秒监控系统里高频调用它会直接把系统拖垮这个坑我亲身经历过——某次告警脚本每分钟扫一遍所有 Java 进程的smaps导致磁盘 IO 和 CPU 双双飙高排查了半天才发现是监控自己干的。5.2 /proc/meminfo 里最容易误读的字段/proc/meminfo是内核内存状态的权威快照字段有六十多个下面挑几个必须搞明白的$ head -20 /proc/meminfo MemTotal: 65863456 kB MemFree: 3355440 kB MemAvailable: 33000000 kB Buffers: 512000 kB Cached: 30000000 kB SwapCached: 2048 kB Active: 20000000 kB Inactive: 15000000 kB Active(anon): 12000000 kB Inactive(anon): 2000000 kB Active(file): 8000000 kB Inactive(file): 13000000 kB Unevictable: 0 kB Mlocked: 0 kB SwapTotal: 8388608 kB SwapFree: 7864320 kB Dirty: 51200 kB Writeback: 0 kB AnonPages: 14000000 kB Mapped: 512000 kB Shmem: 1100000 kB Slab: 3400000 kB SReclaimable: 2800000 kB SUnreclaim: 600000 kB PageTables: 120000 kB几个要点Active/Inactive是 LRU 链表长度不是活跃内存的语义。内核回收时会先在 inactive 里挑不够再把 active 降级。看到Active(anon)特别大说明匿名内存多且访问密集这部分回收起来要去 swap代价高。AnonPages是所有匿名页总和包括堆、栈、私有映射。它和Cached是两大阵营前者是进程的后者是文件的。Mapped是文件映射和共享内存的总和通常远小于Cached因为页缓存里很多页只是被读过没有被任何进程映射。PageTables是页表本身占用的内存。这个值平时几 MB 到几十 MB但如果一个进程映射了几十万个 VMA它能涨到几 GB。我见过一个内存数据库把PageTables顶到 6GB最后发现是映射数量爆炸。CommitLimit和Committed_AS在vm.overcommit_memory2时才有强约束意义前者是允许承诺的上限后者是已承诺量。默认模式 0 下这两个值参考价值有限。5.3 几个 sysctl 参数的取舍与实操记录参数调优最忌讳照抄网上的最佳实践。同样一个vm.swappiness1在数据库服务器上是必要的在内存只有 2GB 的老旧虚拟机上可能是灾难。下面是我自己反复验证过的几个参数以及调整时的判断依据。vm.swappiness控制内核在回收时更倾向于丢页缓存还是换出匿名页。默认 60 意味着两类内存被同等对待。数据库、Redis 这类自己管理内存的服务通常希望尽量别换出匿名页换出后延迟飙升会调成 110而桌面环境希望闲置程序尽快换出、给当前应用让路可以调高。改之前先确认 swap 分区大小——如果 swap 只有 1GB调低 swappiness 基本没意义因为换出空间本来就小。# 临时生效 sysctl -w vm.swappiness10 # 永久生效 echo vm.swappiness10 /etc/sysctl.d/99-memory.conf sysctl --systemvm.overcommit_memory值得说一下。默认值 0 是启发式内核凭经验判断这次分配是否离谱1 是永远答应适合那些自己清楚内存用量的场景比如 Redis 官方明确建议设为 1因为它的 forkCOW 依赖过量承诺2 是严格模式内核按公式CommitLimit swap RAM × overcommit_ratio / 100限制承诺总量。严格模式一旦开错表现是内存明明空着fork却报 Cannot allocate memory排查起来非常反直觉。遇到这类报错先查这个参数别急着怀疑物理内存。vm.max_map_count默认 65530不同内核版本有出入用sysctl vm.max_map_count现场确认。这个值限制单进程 VMA 数量。跑 Elasticsearch、部分 JVM 应用、或者用 mmap 处理超大文件的程序很容易撞上这个限制表现是OutOfMemoryError: Map failed或者mmap: Cannot allocate memory。调大到 262144 一般是安全的代价是每个 VMA 有约 200 字节的内核开销。vm.min_free_kbytes决定内存回收的水位基准。默认值随内存大小自动计算大内存机器可能算出几 GB这个值并非越大越好——它直接降低了可用的缓冲空间太小又会导致直接回收频繁触发。我在一台 128GB 的机器上把它从默认值手动调到 2GB 后kswapd不再频繁唤醒但服务延迟没有变化说明默认值本来就是合适的。没有明确指标支撑的调参本质上是在制造不确定性。6. 高频坑与排查速查表这一节讲的是知道原理之后依然会错的地方。这类问题的共同特点是命令都敲对了数值也都读到了但结论错了。原因通常是没考虑上下文——是容器还是物理机是看总量还是看增量是匿名内存还是文件缓存。6.1 OOM Killer 为什么挑中了你的进程系统内存耗尽时oom_killer会挑一个进程杀掉。选择依据是oom_score计算时考虑几个因素进程的 RSS swap 页表占用越大越容易被杀、是否为特权进程CAP_SYS_ADMIN等会降分、oom_score_adj调整值-1000 到 1000。分数越高越容易被杀。查看方式# 看某个进程的当前分数 $ cat /proc/1234/oom_score 847 # 看调整值 $ cat /proc/1234/oom_score_adj 0最反直觉的一点占内存最多的进程不一定被杀。因为计算时会做开方rss / total × 1000的平方根缩小了差距再加上其他扣分项实际被杀的常常是内存较大但不是最大、又没有特权保护、oom_score_adj还较高的那个。我见过 MySQL 因为配了oom_score_adj -500而幸存旁边一个只占一半内存的日志收集进程被干掉事后被业务方质问为什么杀小的就是这个原因。反过来说如果你想保护某个关键进程做法是# 临时调整需要 root echo -900 /proc/$(pidof mysqld)/oom_score_adj # systemd 服务里永久配置 [Service] OOMScoreAdjust-900别设成 -1000。-1000 表示完全免疫一旦这个进程自己泄漏长成巨兽系统就只能眼睁睁看着它把整机拖死连最后一道保险都没了。生产环境我一般建议 -500 到 -900 之间。6.2 内存泄漏与正常增长怎么区分RSS 一直在涨是最高频的误报。要区分泄漏和正常增长关键是看增长的形态和归属。正常增长通常是启动后快速爬升到平台期之后随业务量波动在内存压力出现时能被换出或回收匿名页进 swap、页缓存被丢弃。泄漏的特征是在业务量稳定的情况下RSS 单调上涨且不回落kswapd反复回收也压不下去最终 OOM。判定的具体操作我一般这么走# 1. 先确认增长的是匿名内存还是文件缓存 $ awk /^Rss:|^Private_Dirty:|^Private_Clean:|^Shared_Clean:|^Anonymous:|^Swap:/ /proc/PID/smaps_rollup # 2. 间隔采样算增长速率 $ for i in $(seq 1 10); do grep VmRSS /proc/PID/status; sleep 60; done # 3. 看具体是哪一段在涨 $ cat /proc/PID/smaps | awk /^[0-9a-f]/{addr$0} /^Rss:/{if($210000) print addr, $0} # 4. 用 bcc 的 memleak 抓分配栈需要内核支持 $ sudo memleak-bpfcc -p PID -a 60第 3 步是最有价值的。它直接告诉你哪一段虚拟区间在膨胀结合区间名称[heap]、[anon]还是某个.so基本能锁定方向。第 4 步的memleak工具需要内核 4.9 并安装 bcc它通过采样分配调用栈来定位泄漏点代价是开起来之后会有额外开销别在生产高峰期随便用。一个大量服务都会遇到的假泄漏glibc arena 碎片。表现为 RSS 缓慢上涨但mallinfo显示uordblks已分配块稳定。这时候不是代码有问题而是分配模式太碎arena 里到处是没法合并的空洞。解决办法是换 jemalloc/tcmalloc或者把MALLOC_ARENA_MAX调小或者干脆用malloc_trim(0)定期主动归还。6.3 常见现象速查表下面这张表是我自己排查时常用的对照覆盖了过去几年里处理过的大多数内存类问题。列的时候把现象、可能原因、验证动作、常见处理放在一起方便直接拿去用。现象可能原因验证动作常见处理free显示 free 极少但服务正常页缓存占用看MemAvailable是否充足无需处理缓存会自动回收RSS 持续涨业务量不变应用泄漏 / arena 碎片smaps分段采样对比修代码 / 换分配器 / 调MALLOC_ARENA_MAXfork报 Cannot allocate memoryovercommit 严格模式 / VMA 上限查vm.overcommit_memory、vm.max_map_count调整对应 sysctlmaj_flt速率很高内存不足反复从磁盘/swap 取页vmstat 1看si/so加内存 / 调swappiness/ 检查热点数据集服务周期性卡顿数百毫秒脏页同步回写阻塞vmstat 1看bo尖峰调低dirty_ratioPageTables涨到 GB 级VMA 数量过多wc -l /proc/PID/maps减少映射数合并小映射内存充足但进程被 OOM 杀cgroup 限制 /oom_score_adjcat /sys/fs/cgroup/.../memory.max调大限制或调整 adjSUnreclaim持续上涨内核缓存/模块泄漏slabtop定位具体缓存升级内核 / 卸载问题模块这张表里的每一条我都至少真实遇到过两次以上能背下来基本能覆盖一线大部分场景。剩下的边角案例靠的是对前面几节原理的理解去推。7. 再往里一层容器、大页与性能观测前六节讲的都是单机视角。现代服务大量跑在容器里内存的可见性和限制方式都变了大页和高性能场景又会引入另一套机制。这一节把这两块补上最后讲两个我自己踩过的真实案例。7.1 cgroup 内存限制与容器里的 free 陷阱容器通过 cgroup 来限制内存。cgroup v1 用的是memory.limit_in_bytesv2 用的是memory.max路径分别是/sys/fs/cgroup/memory/group/和/sys/fs/cgroup/group/。当前用量看memory.usage_in_bytesv1或memory.currentv2。最大的坑是容器里执行free看到的是宿主机的内存不是容器的限制。因为/proc/meminfo是内核全局数据没有做命名空间隔离。一个限制 512MB 的容器里free可能显示 62GB total于是应用启动时按有 62GB 可用去计算堆大小一启动就被 OOM 干掉。解决办法有三种应用侧读取 cgroup 文件而不是/proc/meminfo推荐JVM 从 8u191 之后默认这么做参数-XX:UseContainerSupport挂载 lxcfs 之类的工具它对/proc/meminfo做了一层伪装让容器内看到经过计算的值显式设置应用的内存上限参数-Xmx、GOMEMLIMIT等不让它自己猜。第二个要注意的是memory.high和memory.max的区别。max是硬限制超了直接触发 OOMhigh是软限制超了会限速——分配变慢、回收加急但不会杀进程。生产环境我建议两个都设high设在max的 80%90%这样能在真正 OOM 之前先有一个缓冲带从监控上能看到限速开始的信号有时间介入而不是直接被打死。7.2 透明大页、NUMA 与性能观测透明大页THP是内核自动把 4KB 页合并成 2MB 页的机制好处是 TLB 覆盖范围扩大 512 倍、页表层级减少、缺页次数大幅下降。对内存密集型应用大数组遍历、数据库缓存加速效果明显5%15% 的性能提升很常见。但 THP 有个著名的副作用khugepaged内核线程后台做页合并时会持锁导致某些延迟敏感型应用出现几十毫秒的抖动。所以现在的主流建议是数据库、Redis、低延迟交易类服务用madvise模式echo madvise /sys/kernel/mm/transparent_hugepage/enabled让应用自己决定哪块内存用大页一般业务用always没问题。查看大页使用情况$ cat /sys/kernel/mm/transparent_hugepage/enabled always [madvise] never $ grep -i huge /proc/meminfo AnonHugePages: 512000 kB ShmemHugePages: 0 kB HugePages_Total: 0 HugePages_Free: 0 Hugepagesize: 2048 kBAnonHugePages表示已经被 THP 覆盖的匿名内存量。如果这个值一直是 0说明 THP 没生效可能被never关掉了。NUMA 是另一块。双路服务器上CPU 访问本地节点的内存比访问远端节点快 30%50%。默认策略是就近分配但一个进程在 CPU 0 上启动、之后被调度到 CPU 1它的内存可能还留在节点 0于是所有访问都变成跨节点。观察方式是用numastat看numa_miss和numa_foreign计数。对内存带宽敏感的应用可以用numactl --membind0 --cpunodebind0绑死在一个节点上代价是浪费另一半资源。除非确认跨节点访问是瓶颈否则不要轻易绑 NUMA坑比收益多。观测工具上除了前面提到的/proc系列和slabtop值得掌握的是 bcc 工具集里的几个工具用途典型场景cachestat页缓存命中率判断文件读是否走缓存cachetop按进程统计缓存命中定位哪个进程在 missmemleak分配栈采样定位用户态泄漏点kmem内核分配跟踪定位内核态泄漏oomkill捕获 OOM 事件事后追溯被杀进程这些工具需要内核 4.9 和 bcc 环境在容器里跑还需要--privileged或者挂载/sys/kernel/debug部署前先确认权限。7.3 我踩过的两个真实案例第一个案例是容器里的 Java 服务反复 OOM。现象是容器限制 4GB-Xmx设了 2.5GB按理说很宽裕但运行几小时后必被 OOM 杀。用smaps_rollup一看RSS 只有 3.2GB其中堆占用 2.4GB剩下的 800MB 分散在线程栈200 多个线程 × 1MB 200MB、元空间150MB、JIT 代码缓存240MB、直接内存100MB、还有 glibc arena 碎片100MB 左右。堆只占 RSS 的四分之三其余部分如果按-Xmx去规划必然失手。最终的解法是把-Xmx降到 2GBMaxMetaspaceSize和ReservedCodeCacheSize显式设上限MALLOC_ARENA_MAX2并给容器留出 20% 的余量。调整后稳定运行了大半年。第二个案例是页缓存把监控搞疯了。一台跑着日志聚合服务的机器free显示 used 58GB总 64GB监控告警连着响了三天值班同事一直以为是内存泄漏反复重启服务没用。我接手后先看MemAvailable有 52GB再slabtop一切正常smaps_rollup看各进程 RSS 加起来不到 6GB。剩下的全在Cached里——是日志服务的输出文件以 200MB/s 的速度写入页缓存自然堆满。问题根本不在内存而在于监控用的是used而不是available并且这台机器的日志轮转策略有问题写入量和保留时间都过长。改掉监控指标、缩短日志保留窗口之后告警消失。这两个案例的共同点是卷进去的时候所有人都在讨论一个错误的数字。前者用堆大小当 RSS后者用 used 当可用内存。回到第一节那句话——先把三个词的边界划清楚后面每一步判断才有依托。Linux 内存管理这套机制设计得相当自洽难的不是理解某个单点而是在模糊的现场把它和眼前这堆数字对应上。多做几次采样、多留一份对照慢慢就形成直觉了。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。