Skip to content

内存管理

概念

内存管理是操作系统的核心职责之一,负责协调进程对物理内存的使用。其目标是:让多个进程安全隔离地共享有限的物理内存,同时提供比物理内存更大的地址空间幻觉。

关键术语速览:

术语含义
虚拟地址进程看到的地址,由 OS 映射到物理地址
物理地址实际内存芯片上的地址
页(Page)虚拟地址空间的固定大小块(通常 4KB)
页框(Frame)物理内存的固定大小块,大小与页相同
MMU内存管理单元,硬件完成地址翻译
TLB快表,缓存最近使用的页表项
缺页中断访问未映射到物理内存的虚拟页时触发

核心原理

1. 虚拟内存

为什么需要虚拟内存

  • 进程隔离:每个进程拥有独立的虚拟地址空间,无法直接访问其他进程的内存,防止越界破坏。
  • 地址空间统一:每个进程都从地址 0x0 开始,编译器和链接器无需关心物理内存布局。
  • 按需加载:程序启动时不必全部装入内存,只在访问时才分配物理页,节省内存。
  • 内存超用(Overcommit):进程申请的虚拟内存总量可以超过物理内存,配合换页机制使用。

虚拟地址 → 物理地址翻译(MMU + 页表)

虚拟地址(VA)
    |
    v
+-------+--------+--------+
| VPN2  |  VPN1  | Offset |   (多级页表示例,VPN = 虚拟页号)
+-------+--------+--------+
    |        |
    v        v
  页全局目录 → 页中间目录 → 页表 → 页表项(PTE)
                                        |
                                        v
                                    物理页框号(PFN)
                                        |
                                        v
                               PFN << 12 | Offset = 物理地址(PA)

整个翻译由硬件 MMU 自动完成,OS 负责维护页表数据结构。

进程虚拟地址空间布局(以 64 位 Linux 为例)

高地址
+---------------------------+  0xFFFFFFFFFFFFFFFF
|       内核空间             |  (用户态不可访问)
+---------------------------+  0xFFFF800000000000
|                           |
|       (不可用间隙)        |
|                           |
+---------------------------+  0x00007FFFFFFFFFFF
|         栈(Stack)        |  向低地址增长 ↓
|       (局部变量、返回地址)  |
+---------------------------+
|           ...             |
+---------------------------+
|    内存映射区(mmap 区)    |  动态库、匿名映射
+---------------------------+
|           ...             |
+---------------------------+
|         堆(Heap)         |  向高地址增长 ↑
|       (malloc 分配)        |
+---------------------------+
|      BSS 段(未初始化数据) |
+---------------------------+
|      数据段(已初始化数据) |
+---------------------------+
|      代码段(Text)        |  只读,存放指令
+---------------------------+  0x0000000000400000
低地址

2. 分页(Paging)

页与页框

  • 虚拟地址空间被划分为固定大小的页(Page),物理内存被划分为同等大小的页框(Frame)
  • 典型大小:4KB 字节),地址低 12 位为页内偏移。
  • 虚拟地址 = 虚拟页号(VPN) + 页内偏移(Offset)

页表结构

  • 一级页表:一张大表,32 位地址空间需 个表项,占用 4MB(每进程),开销大。
  • 多级页表:将页表拆成树状结构,只为实际使用的地址范围分配页表节点,节省空间。Linux x86-64 默认使用 4 级页表(PGD → PUD → PMD → PTE)。
  • 每个页表项(PTE)存储:物理页框号 + 标志位(Present / Dirty / Accessed / User/Kernel 等)。

TLB(快表)加速地址翻译

多级页表每次翻译需多次内存访问,性能差。TLB 是 MMU 内部的硬件缓存,存储最近使用的 VPN → PFN 映射。

CPU 发出虚拟地址
       |
       v
   查 TLB ──命中──→ 直接得到物理地址(1 个时钟周期级别)
       |
     未命中(TLB Miss)
       |
       v
   查多级页表(走内存,慢)
       |
       v
   更新 TLB → 返回物理地址
  • TLB 刷新:进程切换时需刷新 TLB(或使用 ASID 标记避免完全刷新)。
  • 命中率通常 > 99%,得益于程序的局部性原理

缺页中断(Page Fault)处理流程

  1. CPU 访问虚拟地址,MMU 发现 PTE 的 Present 位为 0。
  2. CPU 触发缺页中断,陷入内核态。
  3. OS 缺页中断处理程序检查该地址是否合法(是否在 VMA 范围内)。
  4. 若合法:找一个空闲页框,将对应页从磁盘(swap 区或文件)读入。
  5. 更新页表 PTE,设置 Present 位,返回用户态重新执行触发中断的指令。
  6. 若非法(如访问 NULL):触发 Segmentation Fault,进程收到 SIGSEGV 信号。

3. 分段(Segmentation)

段 vs 页

对比项分段分页
划分依据逻辑含义(代码段/数据段/栈段)固定大小块
大小可变固定(4KB)
外碎片有(段间空隙)
内碎片有(页内未用空间)
共享与保护按段粒度,直观需要多个页对齐

段页式(先分段再分页)

将地址空间先按逻辑划分为段,每段再独立分页。地址翻译:段号 → 段基址 + 页表 → 物理地址。Intel x86 保护模式即采用段页式,但现代 OS(Linux/Windows)将所有段基址设为 0,实际退化为纯分页

现代 OS 为什么主要用分页

  • 页大小固定,管理简单,无外碎片。
  • 硬件(MMU/TLB)天然为分页设计优化。
  • 便于换页(swap)和内存映射。
  • 段页式复杂度高,收益有限。

4. 页面置换算法

当物理内存不足、需要换入新页时,OS 必须选择一个页换出到磁盘。

FIFO(先进先出)

按页进入内存的顺序淘汰最早进入的页。实现简单,但性能差。

Belady 异常:分配的物理页框增多,缺页次数反而增加。FIFO 是唯一会出现此异常的常见算法。

LRU(最近最少使用)

淘汰最长时间未被访问的页,基于时间局部性假设。理论上接近最优,但精确实现开销大(需维护访问时间戳或链表)。

实现方式:

  • 双向链表 + 哈希表:每次访问将页移到链表头,淘汰链表尾部。时间复杂度 O(1)。
  • 计数器:每个页记录最后访问时间戳,淘汰时扫描最小值,O(n)。

Clock(时钟算法,近似 LRU)

用一个循环链表和一个指针("时钟指针")模拟。每个页有一个 使用位(R bit)

  1. 访问页时置 R = 1。
  2. 需要换出时,时钟指针扫描:R = 1 则清零并跳过,R = 0 则换出该页。

优点:O(1) 近似 LRU,Linux 实际使用的策略基于此思想(active/inactive 链表)。

OPT(最优算法)

淘汰未来最长时间不会被访问的页。缺页率最低,是理论基准,但因无法预知未来,只用于评估其他算法。

对比总结

算法缺页率实现复杂度Belady 异常实用性
OPT最低不可实现理论基准
LRU接近最优中(需链表+哈希)高(近似实现)
Clock接近 LRU高(Linux 实际使用)
FIFO较高最低

5. 内存映射(mmap)

mmap 系统调用将文件或设备映射到进程的虚拟地址空间。

文件映射 vs 匿名映射

  • 文件映射:虚拟地址区域对应磁盘文件的某个区间,读写内存即读写文件(由 OS 在合适时机同步)。
  • 匿名映射:不对应任何文件,常用于分配大块内存(malloc 大对象底层即用此方式)。

共享映射 vs 私有映射(写时复制 COW)

  • 共享映射(MAP_SHARED):多个进程映射同一物理页,写入对所有进程可见,也写回文件。
  • 私有映射(MAP_PRIVATE):写时复制(Copy-On-Write)。初始时共享同一物理页,某进程写入时内核为其复制一份私有副本,其他进程不受影响。

fork() 就利用 COW:父子进程共享所有物理页,只有在写入时才复制,大幅降低 fork 开销。

主要用途

用途说明
文件 I/O 优化绕过 read/write 系统调用,减少数据拷贝次数
进程间共享内存MAP_SHARED 让多进程共享同一段物理内存
动态库加载.so / .dll 以文件映射方式加载,多进程共享代码段
大内存分配malloc 对 > 128KB 的请求使用匿名 mmap

6. 内存分配

malloc 底层:brk vs mmap

glibc 的 malloc 有两种向 OS 申请内存的方式:

方式触发条件系统调用特点
brk申请 < 128KBbrk / sbrk移动堆顶指针,适合小对象,不立即归还 OS
mmap申请 ≥ 128KBmmap + munmap独立映射,free 后立即归还 OS

128KB 阈值(MMAP_THRESHOLD)可动态调整。malloc 内部维护空闲链表,优先复用已释放的内存块,减少系统调用次数。

内存碎片

  • 内碎片(Internal Fragmentation):分配的块比请求的大,多余部分浪费。例如请求 10 字节,分配了 16 字节对齐块。
  • 外碎片(External Fragmentation):空闲块总量足够,但分散成小块,无法满足大的连续分配请求。

伙伴系统(Buddy System)

Linux 内核用于管理物理页框的分配算法:

  • 内存按 个页框为单位管理(order 0 = 1 页,order 11 = 2048 页)。
  • 分配时找最小满足需求的 order,若无则拆分更大的块(一分为二,两块互为"伙伴")。
  • 释放时检查伙伴是否空闲,若是则合并,反复向上合并,减少外碎片。

Slab 分配器

伙伴系统以页为粒度,对内核中大量小对象(如 task_structinode)效率低。Slab 在伙伴系统之上增加对象缓存层:

  • 为每种固定大小的对象维护一个缓存(slab cache)。
  • 预先分配一批对象,分配/释放时直接从缓存取回,避免反复初始化。
  • 减少内碎片,提升小对象分配速度。

用户态内存分配器对比(高性能服务必背)

面试 Top 题:"你的 Redis / 数据库为什么换 jemalloc?" —— glibc / jemalloc / tcmalloc / mimalloc 是 2026 高性能岗位必背。

4 大主流分配器

分配器出处强项弱项
glibc ptmalloc(默认)GNU兼容性最广、零依赖多线程性能差(早期锁竞争、arena 内碎片大)
jemallocFacebook / FreeBSD多线程优秀、内存碎片少Redis / Firefox / Rust 默认内存占用比 glibc 稍高
tcmallocGoogle吞吐最高(线程本地缓存)、性能 profiler依赖 gperftools
mimallocMicrosoft(2019)最现代、内存占用最低、安全设计(mitigates use-after-free)较新、生态相对小

性能对比真实数字(多线程高并发场景)

维度glibcjemalloctcmallocmimalloc
吞吐1.5-2×2-3×2-3×
内存占用0.7-0.8×0.9×0.6-0.7×
P99 延迟
碎片化严重优秀良好优秀

切换方法(生产实战)

bash
# 方法 1: LD_PRELOAD 不改代码切换(最常用)
LD_PRELOAD=/usr/lib/libjemalloc.so.2 ./myapp

# 方法 2: Docker / 镜像层面预装
FROM ubuntu:24.04
RUN apt-get install -y libjemalloc2
ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
CMD ["./myapp"]

# 方法 3: 代码层面 link
gcc -ljemalloc app.c

💡 jemalloc 真实收益案例

Redis 默认 jemalloc(编译时静态链接),内存碎片率从 glibc 的 30% 降到 5% ② MySQL 切 jemalloc 后高并发 QPS 提升 10-30% ③ 大型 Java 应用 切 jemalloc 后 RSS(实际内存占用)降 15-30% ④ Firefox / Rust 标准库 默认 jemalloc——Rust 1.32+ 改回系统 malloc 是为了通用性,性能敏感应用仍推荐切回

监控分配器(生产必备)

bash
# jemalloc 内置 stats(运行时 dump)
MALLOC_CONF="stats_print:true" ./myapp

# 查看 RSS 真实占用
ps -o pid,rss,vsz,cmd -p <pid>

# 详细分析内存碎片
jemalloc-prof + jeprof  # 火焰图

NUMA 架构(多核服务器必懂)

什么是 NUMA

NUMA(Non-Uniform Memory Access) = 多 CPU socket 服务器上,每个 CPU socket 有自己的本地内存,访问跨 socket 内存会慢。

text
┌──────────────────────────┐      ┌──────────────────────────┐
│      NUMA Node 0          │ ←──→ │      NUMA Node 1          │
│  ┌──────────────────┐    │ QPI/ │  ┌──────────────────┐    │
│  │   CPU 0-15       │    │ UPI  │  │   CPU 16-31      │    │
│  └────────┬─────────┘    │ 互联 │  └────────┬─────────┘    │
│           │              │      │           │              │
│  ┌────────▼─────────┐    │      │  ┌────────▼─────────┐    │
│  │  Local RAM 128GB │    │      │  │  Local RAM 128GB │    │
│  └──────────────────┘    │      │  └──────────────────┘    │
└──────────────────────────┘      └──────────────────────────┘

CPU 0 访问 Node 0 内存: ~100ns(本地)
CPU 0 访问 Node 1 内存: ~150-300ns(跨 socket,慢 50%-200%)

实战命令

bash
# 查看 NUMA 拓扑
numactl --hardware

# 进程绑定到 NUMA 节点
numactl --cpunodebind=0 --membind=0 ./myapp

# 查看进程的 NUMA 内存分布
numastat -p <pid>

# 看 NUMA miss 率(高 = 跨节点访问多)
numastat -m

⚠️ NUMA 经典坑

数据库(MySQL / PostgreSQL / Redis)跨 NUMA 节点写操作可能比本地慢 30-50%——生产 numactl --interleave=all 平均分布或显式绑定。

K8s 1.18+ Topology Manager 支持 NUMA 感知调度——大模型推理 / 高频交易必开。

JVM -XX:+UseNUMA 让 G1 GC NUMA 感知,但仅在 Parallel GC 完全启用。


HugePage / Transparent HugePage

为什么需要大页

问题:标准 page 4KB → 大内存应用页表巨大、TLB miss 率高。

例子:100GB 内存数据库

  • 4KB 页:2500 万个页表项,TLB(512 条)命中率极低
  • 2MB 大页:5 万个页表项,TLB 命中率 99%+

三种使用方式

1. 显式 HugePage(HugeTLB)

bash
# 预分配 1024 个 2MB 大页(共 2GB)
echo 1024 > /proc/sys/vm/nr_hugepages

# 应用 mmap 时指定 MAP_HUGETLB
mmap(NULL, len, PROT_..., MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);

2. Transparent HugePage(THP,自动)

bash
# 三档配置
echo always   > /sys/kernel/mm/transparent_hugepage/enabled    # 总是用
echo madvise  > /sys/kernel/mm/transparent_hugepage/enabled    # ★ 仅 madvise 用(生产推荐)
echo never    > /sys/kernel/mm/transparent_hugepage/enabled    # 禁用

⚠️ THP 在数据库场景是经典反模式

Redis / MongoDB / PostgreSQL 官方都建议关闭 THP 或设 madvise

  • THP 后台 khugepaged 合并小页时会 抖动 latency
  • COW 操作放大(合并 2MB → COW 整个 2MB 而非 4KB)
  • Redis fork RDB 时巨大 COW 导致内存翻倍 OOM

修复:

bash
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
echo never   > /sys/kernel/mm/transparent_hugepage/defrag

Linux 6.x 新增 mTHP(multi-size THP)

Linux 6.1+ 引入 mTHP —— 不再只有 2MB 一档,支持 16KB / 64KB / 1MB / 2MB / 16MB 多种大页,根据 vma 大小自适应。减少 THP 抖动 + 保留大页收益,2026 起主流发行版默认。


OOM Killer(必背)

工作原理

Linux 内存耗尽时杀进程腾内存,由 OOM Killer 决定杀谁。

text
内存紧张

回收 page cache → 不够
回收 anonymous(swap)→ 不够(或没 swap)

触发 OOM Killer

扫描所有进程,按 oom_score 排序

杀分数最高的进程

oom_score 计算

bash
# 查看进程的 OOM 分数(0-1000)
cat /proc/<pid>/oom_score          # 实际分数
cat /proc/<pid>/oom_score_adj      # 用户调整(-1000 到 1000)

# 保护关键进程不被杀
echo -1000 > /proc/<pid>/oom_score_adj    # 永远不杀

# 让某进程优先被杀
echo 1000 > /proc/<pid>/oom_score_adj

评分因素

  • 进程总内存(RSS)越大,分越高 ← 主因
  • 运行时间长 / 子进程多 / 重要进程(如 root 进程)会减分

容器场景:OOMKilled 排查

bash
# K8s Pod 突然 Restart,看是不是 OOMKilled
kubectl describe pod my-pod | grep -A 5 "Last State"
# State: Terminated
# Reason: OOMKilled    ← 关键
# Exit Code: 137       ← 137 = 128 + SIGKILL(9)

# 看宿主机内核日志
dmesg | grep -i "killed process"
# Out of memory: Killed process 1234 (java) total-vm:8GB

详见 K8s — Pod 故障 5 大模式


CPU 缓存与并发底层

理解 CPU 缓存层级、MESI 协议伪共享内存屏障是 2025-2026 年中高级面试区分候选人深度的硬核话题——尤其在写并发库、做性能优化时是绕不开的基本功。

CPU 缓存层级

现代 CPU 普遍是 3 级缓存 + 主存 的层级结构:

┌──────────────────────────────┐
│  寄存器 (Register)            │  延迟 < 1 ns
├──────────────────────────────┤
│  L1 Cache  (32-64 KB / 核)    │  延迟 ~1 ns      ← 每核独占
│  └─ L1d 数据 + L1i 指令       │
├──────────────────────────────┤
│  L2 Cache  (256 KB-1 MB / 核) │  延迟 ~3 ns      ← 每核独占
├──────────────────────────────┤
│  L3 Cache  (8-64 MB / CPU)    │  延迟 ~10 ns     ← 多核共享
├──────────────────────────────┤
│  主存 DRAM (GB 级)            │  延迟 ~100 ns
└──────────────────────────────┘

           "**100 倍**"延迟差距是
           所有性能优化的根本动机

💡 一个不能忘的数字

L1 命中 vs 主存访问,延迟差 100 倍。这就是为什么"减少 cache miss"是高性能代码的核心目标——不是优化算法常数,而是优化内存访问模式

Cache Line:缓存的最小单位

CPU 不会一字节一字节读内存,而是按 cache line(缓存行)整块读写——通常是 64 字节

读 1 字节 a → 实际读了 a 所在的整个 64 字节 cache line
读相邻字节 b → 命中 L1,仅 ~1 ns
读 128 字节后的 c → cache miss,又一次主存访问

核心含义

  • 连续内存访问极快(顺序遍历数组 vs 链表的关键区别)
  • 不同核心修改同一 cache line 上的不同变量也会冲突 → 伪共享

MESI 缓存一致性协议

多核 CPU 都有自己的 L1/L2 缓存,如何保证多个核同时缓存的同一个变量值是一致的?答案是 MESI 协议(Modified / Exclusive / Shared / Invalid)。

四个状态

状态含义何时进入
M (Modified)数据已被本核修改,主存未同步,只此一份本核写入
E (Exclusive)数据未修改,只此一份(其他核没缓存)本核首次读
S (Shared)数据未修改,多核共享这份缓存其他核也读了同一行
I (Invalid)数据已失效,必须重新从主存/其他核获取其他核写入了同一行

状态转换关键流程

核 A 读 x:           Cache A 状态 = E (独占)
核 B 读 x:           Cache A 变 S, Cache B 变 S (共享)
核 A 写 x:           Cache A 变 M, 同时广播 invalidate 给所有共享者
                     → Cache B 变 I (失效)
核 B 再读 x:         Cache B = I → 必须从 Cache A (M) 或主存重新读
                     → 强制 Cache A 把数据写回 → 两个核重新进 S

关键代价核间通信(cache coherence traffic)很贵——这就是为什么"多线程改同一变量"在物理上就比"各改各的"慢得多。

伪共享(False Sharing):最隐蔽的性能杀手

不同线程修改的变量在内存上无关,但因为落在同一个 cache line,导致 MESI 协议反复让对方的 cache 失效

经典案例

java
class Counter {
    long count1;   // 8 字节
    long count2;   // 8 字节
}
// 同一个 Counter 对象的 count1 和 count2 大概率在同一 cache line (64 字节)

// 线程 A 高频写 counter.count1
// 线程 B 高频写 counter.count2
// 表面上无共享 → 实际 MESI 互相 invalidate → 性能下降 10-100×

解决:Padding / @Contended

java
// 方式 1: 手动填充
class PaddedCounter {
    long count1;
    long p1, p2, p3, p4, p5, p6, p7;  // 7 个 long 填满 64 字节
    long count2;
}

// 方式 2: JDK 8+ 的 @Contended (推荐)
import sun.misc.Contended;

class PaddedCounter {
    @Contended long count1;
    @Contended long count2;
}
// 需要 JVM 参数: -XX:-RestrictContended

⚠️ 哪些场景必须考虑伪共享

  1. 高频并发计数器(监控埋点、统计)—— LongAdder 内部就用了 @ContendedCell
  2. 生产者-消费者队列的 head / tail 指针 —— Disruptor 的 Sequence 对象就用 Padding
  3. 线程本地数据数组 —— 不要用相邻索引存不同线程的数据

内存屏障(Memory Barrier)

CPU 为了性能会乱序执行指令;编译器也会做指令重排。内存屏障是一种特殊 CPU 指令,告诉硬件/编译器"这里不能重排"

四种屏障

屏障含义何时插入
LoadLoad屏障前的 Load 必须完成才能执行屏障后的 Loadvolatile 读后
LoadStore屏障前的 Load 必须完成才能执行屏障后的 Storevolatile 读后
StoreStore屏障前的 Store 必须对其他核可见才能执行屏障后的 Storevolatile 写前
StoreLoad最全能、最贵:前面的写对其他核可见后才能执行后面的读volatile 写后 / Unsafe.fullFence()

Java volatile 的本质

Java 的 volatile 关键字底层就是靠 StoreStore + StoreLoad 屏障实现可见性:

java
volatile int x;

x = 1;
// JVM 在这里插入 StoreStore + StoreLoad
// → 确保 x=1 立刻刷到主存 + 其他核 cache 失效

int y = x;
// JVM 在这里插入 LoadLoad + LoadStore
// → 确保读到最新的 x

详见 JVM 内部原理 — Java 内存模型

内存屏障的硬件实现

  • x86:内存模型较强(TSO),大多数操作天然有序,只需 mfence 实现 StoreLoad
  • ARM / Power:弱内存模型,需要更多屏障指令(dmb / dsb),但性能更好
  • 这就是为什么"在 x86 上跑得好的并发代码,移植到 ARM 上可能出现奇怪的并发 bug"

面试黄金回答模板

"CPU 缓存按 64 字节的 cache line 为单位读写,L1 比主存快 100 倍。多核之间靠 MESI 协议保持一致——但任何核修改某 cache line,都会让其他核的同行缓存全部 Invalid,强制重读。这导致两个看起来无关的变量如果落在同一 cache line,会出现伪共享性能塌方,解法是 padding 或 @Contended。

CPU 还会乱序执行指令,需要内存屏障显式禁止重排。Java 的 volatile 关键字本质就是 JVM 在读写处插入了 LoadLoad / StoreStore / StoreLoad 这些屏障,保证可见性和有序性。

这套机制是 LongAdder、Disruptor、ConcurrentHashMap 高性能的物理基础。"


面试常问 & 怎么答

Q1:什么是虚拟内存?为什么需要它?

答题思路:先定义,再说三个核心原因,可提 MMU + 页表机制。

虚拟内存是 OS 为每个进程提供的独立地址空间抽象,由 MMU 将虚拟地址动态映射到物理地址。

需要它的原因:

  1. 进程隔离:每个进程有独立的地址空间,无法随意访问其他进程内存,保证安全性。
  2. 地址空间统一:程序编译时使用固定的虚拟地址,无需关心实际物理内存位置。
  3. 按需加载 + 内存超用:物理内存不足时,将暂时不用的页换到磁盘(swap),进程申请的虚拟空间可超过物理内存总量。

Q2:页面置换算法有哪些?LRU 怎么实现?

答题思路:列出四种算法,重点介绍 LRU 的 O(1) 实现,提 Clock 是实际使用的近似方案。

常见算法:OPT(理论最优)、LRU(最近最少使用)、Clock(时钟,近似 LRU)、FIFO(先进先出,有 Belady 异常)。

LRU 精确实现:双向链表 + 哈希表。

  • 哈希表:页号 → 链表节点,O(1) 查找。
  • 双向链表:维护访问顺序,最近访问的节点移到头部,尾部节点是最久未用的。
  • 访问页:O(1) 查哈希 → 将节点移到头部。
  • 缺页换出:O(1) 移除并返回尾部节点。

实际系统:精确 LRU 需要在每次访问时修改链表,代价高。Linux 使用基于 Clock 思想的双链表(active list / inactive list)近似实现。


Q3:malloc 底层是怎么实现的?

答题思路:分两层回答——用户态 glibc 的空闲链表复用,以及向 OS 申请内存的两种方式。

malloc 分两层工作:

用户态(glibc ptmalloc)

  • 维护多个空闲链表(bins),按大小分类(fast bins、small bins、large bins 等)。
  • malloc 时优先从空闲链表中找合适块复用,避免频繁系统调用。
  • free 时将块放回对应链表,相邻空闲块会合并以减少碎片。

向 OS 申请内存

  • 小于 128KB:调用 brk/sbrk 扩展堆顶,内存释放后不立即归还 OS(留在堆中复用)。
  • 大于等于 128KB:调用 mmap 申请独立匿名映射,free 后调用 munmap 立即归还 OS。

这样设计是权衡系统调用开销与内存归还及时性的结果。


看到什么就先想到这类

关键词 / 场景联想到
进程间内存隔离虚拟内存、页表、MMU
地址翻译慢 / TLB missTLB、多级页表、页大小
程序访问 NULL 崩溃缺页中断 → Segmentation Fault
物理内存不足页面置换算法、swap
多进程共享库mmap 文件映射、共享页
fork 很快 / 写时复制COW(Copy-On-Write)、私有映射
malloc 大对象 vs 小对象brk vs mmap,128KB 阈值
内核分配小对象高效Slab 分配器
物理页框分配伙伴系统(Buddy System)
内存泄漏 / 碎片内碎片 vs 外碎片,堆管理