飞哥讲代码23:C/C++内存空洞

http://lanlingzi.cn/post/technical/2021/0307_code/

5 解决方案
减少内存碎片,整体的原则还是有一些的:

减少动态内存分配,尽量使用栈空间,但栈空间通常只有2M,不能在栈上分配较大对象,否则会导致栈溢出
分配内存和释放的内存尽量在同一个函数,同一个线程,快速回收
不要反复申请释放小内存(<128K)
申请较大的内存时,大小是2的指数次幂,减少跨页
应用层采用内存池来提升内存复用
在应用层做内存池是常见的技巧,不同的场景也有不同的策略:

[1]固定大小内存池:适用于频繁分配固定大小
[2]分箱内存池:适用于可预测的不同固定大小的分配和释放
[3]单线程内存块:申请固定一块内存给此线程使用,简单实现是申请时移动指针,释放不用回收只析构;复杂实现类似实现一个Stack
假定我们的程序是多线程模式,线程间通过队列来分发消息,则我们可以采用上述的[1]与[3]相结合:

队列消息采用固定大小内存池,并支持多线程
其它线程内的对象分配采用大Buffer,线程级复用
当然由于采用不同的策略,申请与释放若不匹配,搞混了就会出现大问题。最为简单的还是集成tcmalloc或jemalloc这类优秀的内存管理框架,减少应用层的感知。真期待我司能有自己开源的malloc框架,而不担心出了问题无法定位,以及涉A问题。

最后简单介绍一下ACE的 ACE_Static_Allocator与ACE_Dynamic_Cached_Allocator 实现,他们正好可用于上述场景。

ACE_Static_Allocator(继承ACE_Static_Allocator_Base),静态内存分配器。它一次性分配大块内存,可用于线程级复用。

ACE_Static_Allocator_Base主要成员变量:

char *buffer_:缓冲区首地址
size_t size_:缓冲区的大小
size_t offset_:当前分配位置
ACE_Static_Allocator_Base主要方法:

malloc:分配指定大小的内存,实质就是buffer_ + offset_ + nbytes,当超过size_时分配失败
free:释放指定的内存块,实质是空操作
ACE_Dynamic_Cached_Allocator, 动态内存分配器。它一次性先分配大块内存,固定大小划分多个chunk,且支持多线程。它内部的结构主要是维护一个空闲链表,链表节点对象为ACE_Cached_Mem_Pool_Node,实现了set_next和get_next

ACE_Cached_Mem_Pool_Node只一个成员变量:

ACE_Cached_Mem_Pool_Node* next_:指向下一个节点
ACE_Cached_Mem_Pool_Node主要方法:

add:加入一个节点到空闲链表
remove:移除一个空闲节点
ACE_Dynamic_Cached_Allocator主要成员变量:

char *pool_:预先申请的大块内存, 总大小为 chunk_size_*n_chunks
ACE_Locked_Free_List< ACE_Cached_Mem_Pool_Node< char >, ACE_LOCK > free_list_: 空闲节点管理
size_t chunk_size_:记录块的大小,用于判断malloc的大小是否超过它,若超过分配失败
ACE_Dynamic_Cached_Allocator主要方法:

malloc:分配内存,实际就是返回 free_list_.remove()->addr()
free:释放内存,实际就是 free_list_.add ((ACE_Cached_Mem_Pool_Node *) ptr),注意,它是直接类型强转哦
ACE_Dynamic_Cached_Allocator的实现巧妙在于free_list_,链表的节点是ACE_Cached_Mem_Pool_Node,它的内存不需要再从其它地方申请,而是直接使用内存池中chunk的内存,每个chunk的内存在池中给Node使用,在池外则上业务使用。

6 结语
频繁的动态内存申请与释放,会留下一些不能使用的内存空间(空洞),这些空洞可能不大,但如果空闲空间分散,形成很多小空洞,可能无法满足新的内存申请要求。系统长期运行后,会是一个严重问题。内存问题,涉及到操作系统的底层知识,水很深,需要我们掌握其原理与机制,才能有针对性减少内存问题,提升程序的运行效率。

Guess you like

Origin blog.csdn.net/wojiuguowei/article/details/121449585