内存管理是计算机编程的一个基本问题。如果管理不善,就会造成内存泄漏,频繁的内存申请和释放往往还会造成内存碎片。
程序申请内存时需要要操作系统打交道,在系统的多进程环境下,这是一个较为耗时的操作。频率的申请甚至可能会成为系统性能的瓶颈。
程序可以在3个地方申请内存,包括全局数据区,栈,堆。全局数据区分配的数据生命周期与程序的生命一样,栈上的数据由系统管理,但是大小有限。程序还可以在堆上申请内存,但是这部分内存申请后由程序管理和释放。如果程序没有释放就退出,就会造成内存泄漏。
通常,内存管理至少有四种流行的方法
- 手工管理
- 引用计数(半自动回收)
- 内存池(半自动回收)
- 垃圾回收器(自动管理)
手工管理内存的语言有C,C++等,利用函数 malloc/free,new/delete等默认函数进行内存的申请和释放。原则上,"谁申请,谁释放",但事实上,这个规则并不时时都很清晰,不管您使用的是哪个分配程序,基于 malloc() 的内存管理器有很多缺点。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。
一种半自动的回收机制,是通过编译器的帮助,可以实现垃圾的回收,而无需编码者费心,常见的引用计数正是这样一种机制。但这种方式需要语言及编译的支持,如C++可以使用这样方式管理内存。但是这种方式适合纯C++的项目,如果项目中还与C联接,则对接是产出的代码也是需要考虑的。
内存池则是应用程序通过系统的内存分配调用预先一次性申请适当大小的内存作为一个内存池,之后应用程序自己对内存的分配和释放则可以通过这个内存池来完成。只有当内存池大小需要动态扩展时,才需要再调用系统的内存分配函数,其他时间对内存的一切操作都在应用程序的掌控之中。
一些动态语言和托管语言都是通过语言自身来管理内存,脚本语言如Perl,Python,托管语言如Java,C#通过自身实现的垃圾收集器不定期的运行收集不再使用的内存,程序员可以在编写代码时几乎不用关心内存的分配和回收。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。它们以程序所知的可用的一组"基本"数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。
表格 1 内存分配策略的对比
策略 | 分配速度 | 回收速度 | 局部缓存 | 易用性 | 通用性 | 实时可用 | SMP 线程友好 |
定制分配程序 | 取决于实现 | 取决于实现 | 取决于实现 | 很难 | 无 | 取决于实现 | 取决于实现 |
简单分配程序 | 内存使用少时较快 | 很快 | 差 | 容易 | 高 | 否 | 否 |
GNU malloc | 中 | 快 | 中 | 容易 | 高 | 否 | 中 |
Hoard | 中 | 中 | 中 | 容易 | 高 | 否 | 是 |
引用计数 | N/A | N/A | 非常好 | 中 | 中 | 是(取决于 malloc 实现) | 取决于实现 |
池 | 中 | 非常快 | 极好 | 中 | 中 | 是(取决于 malloc 实现) | 取决于实现 |
垃圾收集 | 中(进行收集时慢) | 中 | 差 | 中 | 中 | 否 | 几乎不 |
增量垃圾收集 | 中 | 中 | 中 | 中 | 中 | 否 | 几乎不 |
增量保守垃圾收集 | 中 | 中 | 中 | 容易 | 高 | 否 | 几乎不 |
在阅读PostgreSQL的源码时,内存管理通过内存上下文实现。实际上内存上下文也是一个内存池。因此对几种内存管理的方法进行了分析和整理。上面这张表来自《内存管理内幕》,友图中可见不同的内存管理情景下适合的内存管理策略也不同。
参考:
内存管理内幕 http://www.ibm.com/developerworks/cn/linux/l-memory/
C++ 应用程序性能优化,第 6 章:内存池 http://www.ibm.com/developerworks/cn/linux/l-cn-ppp/index6.html