内存是怎么分配、回收并产生碎片的
内存管理最基础的一层,就是分配和回收。
程序运行离不开内存,进程、线程、函数调用、对象创建都要占用内存;程序结束或者某块空间不再使用后,又要把这部分空间收回来重新利用。所以操作系统必须解决两个问题:怎么分,怎么收。
一、为什么需要内存分配和回收
内存是有限资源,但系统里会同时运行很多进程。每个进程都要用内存来存放代码、数据、栈、堆以及各种运行时状态。
因此操作系统需要处理:
- 给谁分配内存
- 分配多少内存
- 从哪里分配
- 用完之后怎么回收
- 回收后怎么继续复用
如果只有分配没有回收,内存迟早会被耗尽;如果没有统一分配,多个进程之间就可能互相覆盖和冲突。
二、从程序视角看,哪些区域和分配回收最相关
一个进程的内存通常可以粗略分成代码区、数据区、堆和栈。其中和“分配、回收”关系最紧密的是堆和栈。
1. 栈
栈上的内存通常跟随函数调用自动分配和自动回收。
常见内容包括:
- 局部变量
- 函数参数
- 返回地址
- 调用现场
函数执行结束后,栈帧弹出,这部分空间也就自动释放了。
栈的特点是:
- 分配和回收快
- 生命周期清晰
- 管理成本低
2. 堆
堆上的内存通常在程序运行过程中动态申请。
常见场景包括:
- C 的
malloc - C++ 的
new - Java 中逻辑上分配在堆上的对象
堆的特点是:
- 更灵活
- 生命周期不固定
- 更容易产生碎片和泄漏
- 管理复杂度更高
所以谈内存分配和回收时,很多问题最终都会落到堆管理上。
三、连续分配和非连续分配
1. 连续分配
连续分配要求给进程找到一整块连续的内存空间。
优点是实现简单、地址计算方便;缺点是随着反复分配和释放,很容易留下很多不连续的小空洞,从而产生外部碎片。
2. 非连续分配
非连续分配不要求整个进程都放在连续物理内存里,而是允许把内容分散放到多个位置。分页就是典型例子。
它的优点是更灵活,也更容易利用零散内存;缺点是管理结构会更复杂,需要页表、地址转换等机制配合。
四、什么是内存碎片
内存碎片指的是:系统里明明还有空闲内存,但这些空闲空间分布得不规整、不连续,导致利用率下降,甚至无法满足新的分配请求。
也就是说,不一定是“没有内存了”,而是“内存变得不好用了”。
碎片通常来自频繁的分配和释放:
- 申请大小不同
- 释放顺序不同
- 空闲块被切得越来越零散
五、内部碎片和外部碎片
1. 内部碎片
内部碎片是指:已经分配出去的内存块内部,没有被真正用满的那部分空间。
比如系统按 4KB 一页分配,你只用了 6KB,却拿到了 8KB,那么多出来的 2KB 就是内部碎片。
可以记成:
分配出去了,但没用满。
2. 外部碎片
外部碎片是指:空闲内存总量虽然足够,但被切成了很多分散的小块,无法拼成一块足够大的连续空间。
可以记成:
还没分出去,但太零散。
六、为什么连续分配更容易产生外部碎片
因为连续分配要求每次都找到一块足够大的连续空间,但不同进程申请和释放内存的大小、顺序都不一样。时间久了,内存中就会留下很多彼此分隔的小空洞。
这些空洞总量可能不小,但未必能组成一块满足新请求的大空间,所以连续分配更容易产生外部碎片。
七、为什么分页能缓解外部碎片,但又会带来内部碎片
分页把虚拟地址空间和物理内存都切成固定大小的页和页框。分配时不再要求找到一整块连续物理内存,只要找到若干个空闲页框即可。
因此:
- 分页基本缓解了外部碎片问题
- 但由于页大小固定,最后一页可能用不满,所以仍然会有内部碎片
这也是分页和连续分配的重要区别。
八、总结
内存分配和回收是内存管理的起点。分配负责把可用内存交给进程或对象使用,回收负责把不再使用的空间重新变成可用状态。栈上的内存通常自动管理,堆上的内存更灵活但也更复杂。频繁分配和释放会导致内存碎片,其中内部碎片是“分出去了但没用满”,外部碎片是“没分出去但太零散”。分页通过固定大小分配缓解了外部碎片,但仍然可能带来内部碎片。
