C/C++赋予程序员管理内存的自由,是C/C++语言特色,虽然这引入了复杂度和危险性,但另一方面,它也增加了控制力和灵活性,是C/C++独特之处,亦是强大之处。
动态内存分配器
libc是c语言的标准程序库,glibc是c标准程序库在linux系统下的一个实现。动态内存分配器(例如glibc ptmalloc)是介于操作系统和应用程序之间的一个程序库,从kernel程序员的视角看来,它属于应用程序库,因为free掉的内存不一定真正返还系统,而应用程序员看来,它又属于偏系统级程序,因为free掉之后,应用程序似乎已经完成了归还。
虚拟存储
操作系统管理物理内存,运行在操作系统上的应用程序,拥有独立的虚拟地址空间,操作系统提供从虚拟地址到物理地址之间的映射,这种转换是自动的,需要硬件(寄存器)和软件(页表)配合完成,虚拟内存是计算机的一个核心概念,基于虚拟内存而不是物理内存,使得运行在单机上的多个进程之间共享存储变得可能。我们用c/c++开发应用,使用的地址都是虚拟地址,如果访问的虚拟地址不在内存里,则产生缺页中断,操作系统会自动换页,而这个过程是有成本的,惩罚还不低,所以,我们应该了解和利用局部性,这对指令和数据同样有效。
linux进程的虚拟地址空间
下面的两张图描绘了linux进程的虚拟地址空间,理解这个很重要。
CACHE
CPU、寄存器、(多级)缓存、内存、磁盘IO各级之间的速度至少相差一个量级,没有什么是cache解决不了的。
我们应该利用局部性,编写和选择局部性好的算法,比如基于有序数组的二分查找往往比rbtree更快。
应用程序内存管理
用c/c++编写应用程序,经常接管动态内存分配和回收,主要基于以下几方面的考虑。
快,希望动态内存的请求能够得到迅速满足,动态内存分配器吞吐率越大越好。
省,减少内存碎片,包括内碎片(chunk块内部填充)和外碎片,有更好的内存利用率。快和省往往是此消彼长的,内存管理常需平衡折中。
稳,期望更好的安全保障,比如避免泄漏、悬垂指针等。
基于以上需求,形形色色的内存管理技巧被发掘出来。
比如nginx基于pool的技术,让每个request从单独的pool对象里分配内存,pool通过移动指针(快)满足动态内存请求,request处理过程中不需要为每个分配的chunk做释放动作,只需要在request处理完成阶段释放pool,从而释放过程中申请的每个chunk,从而拥有更好的安全性。
小对象分配,如果void *p = malloc(1)分配一个字节,实际上因为有首尾部和填充,所以实际上消耗远远大于一个字节,这体现在用malloc_usable_size(p)返回值远大于1字节。有效载荷跟实际占用的比值应该是越大越好,而小对象有效载荷比较小,所以有必要优化它,这对于频繁且大量分配小对象的应用程序是有效的,有很多针对小对象的分配优化方法,比如经典的《C++设计新思维》提到的小对象分配器。
对象池,针对一类对象的专用分配器,因为libc的动态内存分配器是为通用目的而设计的,它在最广泛情况下表现良好,但不是每种情况都表现最好,所以针对特定场景和需求而专门设计内存分配策略,是有效的。如果应用程序,有一类对象,最多只分配1000个,便可以用这类对象池,它几乎同时满足了快、省、稳的要求。
预分配、有些业务场景,会在服务启动时候把内存都分配好,运行过程中,不再动态申请,这在游戏服务器等业务中经常出现,初听有些奇怪,实际上,它也带来一些好处,比如程序运行过程中,你不用担心OOM的问题,因为理论上它的内存占用量是直线,而且基于预分配的策略,内存泄漏的问题也会少很多。
语言和工具支持
c语言支持hook malloc、free、realloc等接口,这样你可以从比较低的层次干预和统计内存分配。
c++支持operator new/new[]、operator delete/delete[]、以及类的operator new/delete重载。
c++的标准库容器,比如vector、list、map等,都支持传入自定义allocator,你可以接管内存配置,而不限于默认分配器。
COW(Copy On Write)写时拷贝是一项能节省拷贝的技术,fork出来的进程也用到了cow,如果要全量拷贝,那fork的返回会延迟很多。
为了防止内存泄漏,有时候会借助RAII技术。
引用计数是实现智能指针的关键技术,需要区分弱引用和强引用,以及shared和unique,所有权的概念。
ptmalloc可以开启一些统计选项,这可以为排错提供帮助。
libc的动态内存分配器默认是ptmalloc,你也可以用google的tcmalloc,以及jemalloc等动态内存分配器替换。
监控和查找内存泄漏问题,你可以借助valgrind工具,不过valgrind对代码有要求,只有符合它期望的程序才能valgrind干净,不然误报会比较多。
内存拷贝是我们应该竭力减少的,不做任何多余的拷贝,很多程序profiling会发现,内存、字符串相关的函数经常出现在top list。