🧠 内存管理
虚拟内存 · 分页分段 · 页面置换 · mmap 与零拷贝
1. 什么是虚拟内存?为什么需要?
虚拟内存:每个进程有独立的虚拟地址空间(64 位 Linux 上约 128TB),通过页表映射到物理内存。进程看到的地址 ≠ 物理地址。
三个价值:
- 隔离与安全:进程间地址互不可见,A 进程崩溃不影响 B
- 内存不足也能跑:物理内存不够时,不常访问的页换出(swap)到磁盘,用的时候换入(缺页中断)——虚拟内存 > 物理内存
- 简化编程:程序不用管物理内存碎片和布局,地址连续
🎯 面试要点
- 页表开销:每个进程一张页表 → 多级页表 + TLB(快表)缓存最近映射,TLB miss 才查内存
- Java 视角:JVM 堆是虚拟地址,物理内存不足时可能 swap(性能暴跌)→ 监控要关注 swap 使用
- free 命令的 buff/cache:PageCache 占"看起来"很多内存是正常的(可回收)
2. 分页和分段的区别?
- 分页:固定大小(4KB)的内存块,无外部碎片,有内部碎片(<4KB 浪费)。现代 OS 用分页
- 分段:按逻辑段(代码段/数据段/栈)划分,大小可变,符合程序逻辑;有外部碎片(碎片整理难)
- 实际:分段 + 分页结合(段表 → 页表两级映射)
- 缺页中断:访问的页不在物理内存 → 触发缺页异常 → 从磁盘换入 → 更新页表 → 恢复执行。频繁缺页 = 抖动(thrashing)
🎯 面试要点
- 内部碎片 vs 外部碎片:分页内碎片(每页浪费 <4KB)、分段外碎片(缝隙无法利用)
- 大页(Huge Pages 2MB/1GB):减少页表项、TLB miss → JVM/数据库优化参数(-XX:+UseLargePages)
3. 页面置换算法?
- FIFO:先进先出,简单但可能换出高频页(Belady 异常)
- LRU(最近最少使用):换出最久未用的——最优近似,硬件实现成本高
- LFU(最不经常使用):按访问频率,防"偶用一次"被误换
- Clock(时钟算法):LRU 的近似实现——页带访问位,循环扫描,访问位为 1 清 0 继续,遇 0 换出。Linux 用的是 active/inactive 双 LRU 链表 + young 位的二次机会式近似——注意教材里的"改进型 Clock"指 访问位 + 修改位,与 Linux 的实现不是一回事
🎯 面试要点
- 最优置换(OPT)只存在于理论(预知未来)——LRU 是它的实际近似
- Redis 的近似 LRU 也是 Clock 思路(抽样)——跨模块呼应
4. mmap 是什么?与普通 IO 的区别(零拷贝)?
- mmap:把文件映射到进程虚拟地址空间——读写文件像读写内存(缺页时按页从磁盘加载,写回由内核 flush)。省去 read/write 系统调用的用户态↔内核态拷贝
- 普通 IO 的 4 次拷贝:磁盘 → 内核缓冲 → 用户缓冲 → 内核 socket 缓冲 → 网卡(4 次拷贝 = 2 次 DMA + 2 次 CPU 拷贝,另有 4 次上下文切换,二者不是一一对应)
- mmap + write(零拷贝):磁盘 → 内核缓冲(mmap 映射)→ socket 缓冲 → 网卡,省 1 次用户态拷贝
- sendfile(真零拷贝):磁盘 → 内核缓冲 → 网卡(DMA 直接),全程无用户态参与——Kafka/Netty 文件传输用
Java 中的 mmap
// MappedByteBuffer:直接映射文件,省拷贝
FileChannel channel = FileChannel.open(path, StandardOpenOption.READ);
MappedByteBuffer buf = channel.map(FileChannel.MapMode.READ_ONLY, 0, channel.size());
// 后续像操作 ByteBuffer 一样读文件(缺页按需加载)
🎯 面试要点
- mmap 风险:映射大文件占虚拟地址空间、写回时机不可控(可用 msync 强制)
- 零拷贝是 IO 性能面试的王牌答案:Kafka 消费(sendfile)+ Netty(mmap/DirectBuffer)