stringbuilder调用tostring常量池_C++ 内存管理与内存池

6cbcd44e61c60500c6b9e3d4c19c6df3.png

C++的最大优势就在于可以时时刻刻将内存掌控在自己手上。本文总结了几篇相关博文并加以自己的理解。首先整理了内存管理相关知识,之后以STL和Nginx内存池为例学习内存池的设计。如有错误请多多指教。

C++内存管理

了解内存的第一步是探究C++如何管理内存。

c++中内存分为5个区:

  • 栈。在执行函数时,局部变量的存储单元都可以在栈上创建,函数执行结束时存储单元自动释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆。由malloc分配的内存块。释放由应用程序控制。
  • 自由存储区。由new等分配的内存块,由free来释放。
  • 全局/静态存储区。全局变量和静态变量被分配到同一块内存中。
  • 常量存储区。存放为常量,不允许修改。

堆与栈

void func() {
    int* p=new int[5]; 
}
  • 该函数同时包括了堆与栈的使用。其中指针p为局部变量,存在栈中,而new通过调用operator new在堆中分配一块内存并返回内存首地址,存入栈中。

栈与堆的区别:

  • 管理方式:栈由编译器管理。堆由程序员手动控制。
  • 空间大小:栈的空间大小为编译器设置,默认大小较小。堆的大小通常达到数个GB。
  • 碎片问题:频繁的new,delete会导致堆中生成大量内存碎片。而栈采用FILO机制,不会存在碎片。
  • 分配方式:栈分为静态分配动态分配。静态分配在编译时期分配,动态分配由alloca分配。
  • 分配效率: 栈的效率是要高于堆的。栈的操作由CPU底层支持,分配专门的寄存器和CPU指令。而堆的分配由库函数按照一定算法支持。

C++控制内存分配

C++使用new或delete可以很轻松的操控内存,但也很容易引起内存破碎。防止内存破碎的一个方法就是从不同固定大小的内存池中分配不同类型的对象。对每个类重载new和delete可以帮助程序实现该方法。

class Test {
public:
    void* operator new(size_t size);
    void operator delete(void *p);
};
void *Test::operator new(size_t size){
    void *p = malloc(size); // C++库下的operator new仅仅对malloc进行一个封装,要使用内存池,需要在
    return (p);             // 此处重新设计内存分配函数
}
void Test::operator delete(void *p){
    free(p); // C++库下的operator delete仅仅是对free的一个简单封装
}

要注意的是,我们同样需要重载new[]delete[],否则在调用这两者时会默认调用全局的函数,导致内存无法释放。

内存使用错误和对策

  • 内存分配未成功却使用了它。解决方法为在内存使用之前检查指针是否为nullptr
  • 内存分配成功,但未初始化。创建时应赋初始值。
  • 忘记释放内存。new和delete必须成对使用,防止内存泄漏。
  • free或delete释放内存后,立即将指针设置为nullptr,防止产生“野指针”。

函数传递指针

void GetMemory(char *p, int num){
   p = (char *)malloc(sizeof(char) * num);
}
void Test(void){
   char *str = NULL;
   GetMemory(str, 100); // str仍为nullptr
   strcpy(str, "hello"); // 运行错误
}
  • 该程序运行错误。编译器要为每个函数的每个参数创建临时副本。比如此处p创建了副本_p,即 _p = p。函数内为 _p分配了一块由malloc返回的内存,返回地址赋值给p。但这只是改变了副本 _p的值,并未改变p,导致GetMemory函数出现内存泄漏。
  • 可以通过返回指针的方式分配内存,但切记不可返回栈内的指针,如直接创建数组返回,必须返回指向内内存的函数指针。

野指针

野指针定义为指向不确定内存空间的指针,并非是空指针。指针未被初始化,指针被free或delete后,未被设未nullptr指针超过变量作用域都有可能造成野指针的产生。使用正确的使用策略,可以避免野指针的出现。

为什么要new和delete

malloc和free是标准库函数,而new和delete是c++运算符。都用于申请和释放动态内存。

但当需要构造非内部对象时,使用malloc/free无法满足需求。希望在分配内存后,要自动执行构造函数,在对象消亡时需要自动执行析构函数并释放内存。而malloc/free是库函数,不受编译器权限控制,无法承担构造和析构对象的责任。

同时,我们要保留malloc和free。因为C++时常需要调用C函数,C函数是不支持free和delete的。

malloc/free与new/delete使用

void * malloc(size_t size); //malloc原型
int *p = (int *) malloc(sizeof(int) * length); //调用malloc
void free( void * memblock );
  • malloc返回void*空指针。需要强制类型转换为期望的类型。
  • malloc不识别申请内存的类型,只关心字节数。通过sizeof()判断。
int *p2 = new classA(); //调用new
  • new内置了sizeof,类型转换和类型安全检查函数,在分配内存同时完成了初始化工作。
  • 如上文所述,调用new构造数组时,切记使用delete[]告诉编译器释放的对象是数组。

STL内存管理和内存池

SGI STL早期使用内存池技术,自定义了空间配置器alloc。之后版本使用allocator。

allocator< T >

e374eb063b82c27420550fd9e7a07b36.png
  • 用户代码创建vector模板。同时,实例化一个allocator模板,负责对象T的分配,释放还有对象的构造和析构。所以需要知道类型进行对象的析构和构造。

在STL标准下,allocator是有标准的

// 配置空间,足以存储n个T对象
pointer allocator::allocate(size_type n, const void* = 0)
// 释放空间
void allocator::deallocate(pointer p, size_type n)
// 调用对象的构造函数,等同于 new((void*)p) T(x) 
// new((void*)p) T(x) 为placement new,即在指定内存空间下构造函数
void allocator::construct(pointer p, const T& x)
// 调用对象的析构函数,等同于 p->~T()
void allocator::destroy(pointer p)
  • allocator最重要的是以上四个函数。可以简单理解为,前两个分配释放内存,后两个构造析构对象。

SGI alloc

SGI在STL标准之外自定义了一个空间配置器。

SGI alloc定义在<memory>中,其包含三个文件:

  • <stl_construct.h>:定义了全局函数construct()和destroy(),负责对象构造和析构。SGI STL的alloc将空间配置和对象构造分为两个阶段操作。
  • <stl_alloc.h>:负责内存配置和释放,其内部有两级配置。第一级结构简单。第二级实现了自由链表和内存池,提升大量小额内存配置时的性能。
  • <stl_uninitialiezed.h>:定义用于用于填充和拷贝大块内存的全局函数。有利于大规模元素的处置设置以及之后大规模拷贝操作。

48f755bcc16cc7cb2d6752ebaa3be925.png
  • 此处,当我们实例化一个vector对象后,不再需要实例化一个allocator模板,而是直接调用STL全局的alloc作为空间配置器。
  • 从图中可以看出,alloc定义了两级空间配置器。第二级配置器实现了内存池和自由链表,容器可以直接从自由链表和内存池中获取空间。这样设计目的是考虑到内存破碎问题。小内存块设计为内存池和自由链表可以减少系统调用,提升性能。而对于大内存空间,采取直接调用一级配置器,即直接调用封装的malloc和free配置空间。

SGI STL 空间配置器的第二级配置器

使用内存池作为二级配置器有两个优点:

  1. 避免太多小额区块造成内存的碎片。
  2. 配置时的额外负担。每申请一块内存,都需要一段cookie保存该段内存相关的信息。当区块较小时,这样的负担所占的比例会更大。

SGI第二级空间配置器下,如果区块够大,超过128bytes时,就移交第一级配置器。小于128bytes,则由内存池管理。内存池的内存块通过自由链表维护。同时,为了便于管理,配置器会将小额区块的内存需求上调为8的倍数。SGI内存池总共维护16个free-lists,管理从8bytes到128bytes大小的区块。可以看出,STL内存池为一个固定大小的内存池,方便管理,但会造成空间的浪费。

union obj {
    union obj* free_list_link;
    char client_data[1];
};
  • 以上代码为free-lists节点的结构。我们知道内存池由链表连接在一起,如果每个节点都有额外指针,会导致资源的浪费。故使用union关键字,当未分配时作obj视为指针,分配后视为对象。
enum {__ALIGN = 8}; // 8bytes对齐
enum {__MAX_BYTES = 128}; // 管理区块上界
enum {_NFREELISTS = __MAX_BYTES/__ALIGN}; // free-lists个数
// 二级配置器
template <bool threads, int inst>
class __default_alloc_template {
private:
    // 都申请为静态,方便全局调用
    static size_t ROUND_UP(size_t bytes) { // 将大小上调至8的倍数
        return ((bytes) + __ALIGN-1) & ~(__ALIGN-1);
    }
    union obj { // free-lists的节点
        union obj *free_list_link;
        char client_data[1];
    };

    static obj *volatile free_list[_NFREELISTS]; // 申请一个指针数组,数组元素为obj*
    static size_t FREELIST_INDEX(size_t bytes) { // 根据ROUND_UP结果计算应该分配第几号free-lists
        return ((bytes) + (__ALIGN-1)) / (__ALIGN-1);
    }
    // 没有可用区块,调用free-lists填充空间
    static void *refill(size_t n);
    // 配置空间,可容纳 nobj 个大小为size的区块,如果剩余空间不够,降低nobjs数量
    static char *chunk_alloc(size_t size, int &nobjs);

    // Chunk allocation state
    static char *start_free;    // 内存池起始位置
    static char *end_free;      // 内存池结束为止
    static size_t heap_size;

public:
    static void *allocate(size_t n);
    static void deallocate(void *p, size_t n);
    static void * reallocate(void *p, size_t old_sz, size_t new_sz);
};
  • 以上代码定义了一个alloc模板类,定义了内存池。
  • 注意到,定义内存池的若干个free-lists数组时,使用了volatile关键字,告诉编译器数组的地址随时可能变化,每次访问都要从内存中取值,而不是寄存器。
template<bool threads, int inst>
void * __default_alloc_template<threads, inst>::allocate(size_t n) {
    obj * volatile * my_free_list; 
    obj * result;
    if (n > (size_t) __MAX_BYTES) { // 大于128bytes的区块交由allocate处理
        return malloc_alloc::allocate(n);
    } 
    my_free_list = free_list + FREELIST_INDEX(n); // 找到size对应的free-lists,获得可用内存地址。
                                                  // free_list为数组的首地址,my_free_list为数组内元素的地址。
    result = *my_free_list;  // 根据volatile修饰词,必须从内存中读取。表示数组中的一个元素,该元素为指针obj*
    if (result == 0) { // nullptr 
        void *r = refill(ROUND_UP(n)); // 没找到可用的free list,准备重新填充free list,并将第一块内存分配出去
        return r;
    }
    *my_free_list = result -> free_list_link; // free list首地址变为下一个节点,如下图3
    return result;
}

8bafc41e6cd64b360187393d0246c6c5.png
  • 注意这里用到的obj * volatile * my_free_list;的原型为obj ** my_free_list;。其为一个双重指针,为地址的地址。因为my_free_list要表示的是free-list指针数组中的一个元素,所以my_free_list要使用双重指针。而volatile放在*的中间,修饰 *my_free_list,向编译器指明数组中的元素是volatile的。
  • 也可以拆分开来看,后面的my_free_list表示指针数组,前面的obj 表示了数组内的元素为该类型。
// 分配内存,返回一个大小为n的区块,可能将大小为n的其他区块加入到free_list
static void* refill(size_t n){  
    int nobjs = 20; // 默认分配20个区块
    char* chunk = chunk_alloc(n, nobjs); // 获得分配的内存空间
    obj* volatile * my_free_list;
    obj* result, *current_obj, *next_obj;
    // 如果只分配了一个区块,直接返回
    if (1 == nobjs) return chunk;
    // 否则将其他区块插入到free list
    my_free_list = free_list + FREELIST_INDEX(n);
    result = (obj*)chunk; // 从chunk中切分下一块
    *my_free_list = next_obj = (obj*)(chunk + n); // 将后面的区块插入到free list,n表示切割的大小
    for (int i = 1;; ++i){ // 切割内存块,并用指针相链,避免大量cookie产生
        current_obj = next_obj;
        next_obj = (obj*)((char*)next_obj + n);  
        if (nobjs - 1 == i){ // 最后一个指向空指针
            current_obj->free_list_link = 0;
            break;
        }
        current_obj->free_list_link = next_obj;
    }
    return result;
}
  • refill的设计思路为为相应大小的free lists分配默认20个内存块,如若不够,则可能小于20个。实际个数由分配函数chunk_alloc决定。
  • 注意代码中如何切割特定大小的区块。一开始返回的chunk由char*指向,通过n的大小控制切割区块的大小。
template <bool threads, int inst>
char *
    __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) { // 内存池内存分配函数
        char * result;                                                              // nobjs通过引用传递,方便更改实际申请的内存块个数
        size_t total_bytes = size * nobjs; // 内存需求大小
        size_t bytes_left = end_free - start_free; // 内存池剩余空间
    if (bytes_left >= total_bytes) { // 内存池剩余空间完全满足需求量     
        result = start_free;
        start_free += total_bytes;
        return result;
    } else if (bytes_left >= size) { // 内存池剩余空间不能完全满足需求量,但足够供应一个(含)以上的区块  
        nobjs = bytes_left/size; // 计算最多供应的区块数
        total_bytes = size * nobjs;
        result = start_free;
        start_free += total_bytes;
        return result;
    } else { // 内存池剩余空间连一个区块的大小都无法提供 
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4); // 申请大小为需求的两倍,并加上附加量
        if (bytes_left > 0) {  // 利用剩余的内存,避免碎片
            obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left); // 找到适合该大小的free-list
            ((obj *)start_free) -> free_list_link = *my_free_list; // 调整free list,将内存池中的剩余空间加入指定的free-list
            *my_free_list = (obj *)start_free;
        }
        // 申请堆内空间,补充内存池
        start_free = (char *)malloc(bytes_to_get);
        if (0 == start_free) { // 申请失败
            int i;
            obj * volatile * my_free_list, *p;
            for (i=size; i <= __MAX_BYTES; i+=__ALIGN) { // 寻找其它free-list手上是否有空闲的内存
                my_free_list = free_list + FREELIST_INDEX(i);
                p = *my_free_list;
                if (0 != p) { // free list内有未用块
                    *my_free_list = p -> free_list_link; // 调整free list以释放未用区块
                    start_free = (char *)p;
                    end_free = start_free + i;
                    return chunk_alloc(size, nobjs);   // 递归调用自己,为了修正nobjs
                }
            }
            end_free = 0; // 无任何空闲内存,则调用第一级配置器
            start_free = (char *)malloc_alloc::allocate(bytes_to_get);
        }
        heap_size += bytes_to_get; // 每次申请大小累加,用于附加内存申请大小的计算
        end_free = start_free + bytes_to_get; // 修改内存池
        return chunk_alloc(size, nobjs);  // 递归调用自己,为了修正nobjs
    }
}

5f8702c71aa87df8845254d8ca501c6d.png
  • 如内存池耗尽去申请内存,申请大小为需求的两倍再加上附加值。每次申请的附加值大小为heap_size除以16后的大小。
  • 分配函数最后总以递归调用结束,直到能有返回值。个人理解,递归调用保证了代码一定的简洁性。当需要调用chunk_alloc到堆中分配内存后,不用重复先前的步骤,而是递归调用,保证能将内存成功分配。而其另一个显现的作用是在分配堆内存后,分配正确数量的区块返回。

Nginx内存管理和内存池

Nginx作为一个服务器反向代理组件,必须满足长期稳定运行的要求。所以它在内存池的设计上是十分精巧的。

与STL内存池思路相同,内存池代替了OS的部分职责,接管内存分配。程序不用直接和OS交互,减少了频繁的系统调用,提升运行效率。同时降低操作系统的内存碎片,避免内存泄漏,使得服务器可以长期稳定运行。Nginx的内存池是不固定大小的。

基本数据结构和内存池搭建

typedef struct {
     u_char *last; // 保存当前内存池以分配地址的尾,即下一次分配的开始位置
     u_char *end; // 内存池结束位置
     ngx_pool_t *next; // 指向下一个内存池
     ngx_uint_t failed; // 内存池分配失败次数,失败一次加1
} ngx_pool_data_t;
  • 记录一个内存池
struct ngx_pool_large_s {
    ngx_pool_large_t *next;
    void *alloc; // 指向分配的大块内存
};
  • 大块数据分配
struct ngx_pool_s {
     ngx_pool_data_t d; //内存池的数据块,该内存池头
     size_t max; // 内存池数据块最大值
     ngx_pool_t *current; // 指向内存申请开始的位置
     ngx_chain_t *chain; 
     ngx_pool_large_t *large; // 大块内存链表
     ngx_pool_cleanup_t *cleanup; // 所有待清理的资源链表
     ngx_log_t *log; //日志
};
  • 整个内存池的初始化定义

98632fb1fcca0c59d651115ab5d6f4f6.png
  • 上图即为Nginx整个内存池的构造。其实际是由多个内存池组成。ngx_pool_data_t下的next指向下一个内存池,当当前内存池用完时,便初始化一个新的内存池,构成一个内存池链。从上图中可以看出,第一个内存池有完整的ngx_pool_s,用来记录日志等信息,而之后的只有ngx_pool_data_t来记录当前内存池的状态。这一整个内存池链用于分配小块的内存。
  • 注意到ngx_pool_s的current变量。该变量指向可分配内存的开始位置。如第一个内存池用完,那么current指向下一个内存池。current指向是由一定算法决定的,通过不同的内存池failed字段的值,改变current的指向。由此,当下次访问,会直接跳过第一个内存池,从第二个内存池开始分配。该机制保证了Nginx对性能的极致追求。一般来说,failed大于4后,current向后移一个内存池。
  • ngx_pool_s的large变量用来指向大块内存。当要释放时,遍历large链表释放。

Nginx内存池总体构想

e4fb384ff50127109da9cc1937095282.png

内存池函数

创建内存池ngx_pool_t * ngx_create_pool(size_t size, ngx_log_t *log);
销毁内存池void ngx_destroy_pool(ngx_pool_t *pool);
重置内存池void ngx_reset_pool(ngx_pool_t *pool);
内存申请(对齐)void * ngx_palloc(ngx_pool_t *pool, size_t size);
内存申请(不对齐)void * ngx_pnalloc(ngx_pool_t *pool, size_t size);
内存清除ngx_int_t ngx_pfree(ngx_pool_t pool, void p);

内存分配

着重关注Nginx内存池是如何分配内存的。

//分配内存对齐NGX_ALIGNMENT的块
void *
ngx_palloc(ngx_pool_t *pool, size_t size){
    if (size <= pool->max) {
        //分配小块内存
        return ngx_palloc_small(pool, size, 1);
    }
    //分配大块内存
    return ngx_palloc_large(pool, size);
}
//分配内存大小size的块,不做对齐
void *
ngx_pnalloc(ngx_pool_t *pool, size_t size){
    if (size <= pool->max) {
        //分配小块内存
        return ngx_palloc_small(pool, size, 0);
    }
    //分配大块内存
    return ngx_palloc_large(pool, size);
}
  • 内存分配分为大内存区块和小内存区块。小区块从内存池中分配,大区块挂载到large链表下。

分配小块内存

// 分配小内存块
static ngx_inline void *
ngx_palloc_small(ngx_pool_t *pool, size_t size, ngx_uint_t align)
{ // 参数为内存池,分配大小,是否对齐
    u_char      *m;
    ngx_pool_t  *p;
    p = pool->current; // 指向分配开始 
    do {
        m = p->d.last;
        if (align) {
            m = ngx_align_ptr(m, NGX_ALIGNMENT); //内存对齐NGX_ALIGNMENT的块
        }
        if ((size_t) (p->d.end - m) >= size) { // 计算end值减去这个偏移指针位置的大小是否满足索要分配的size大小
            p->d.last = m + size; // 满足,则移动last指针位置,并返回所分配到的内存地址的起始地址
            return m;
        }       
        p = p->d.next; //如果不满足,则查找下一个链,即下一个内存池
    } while (p);    
    //遍历完整个内存池链表均未找到合适大小的内存块供分配,则执行ngx_palloc_block()来分配。  
    return ngx_palloc_block(pool, size);
}

分配新的内存池,链接上内存池链

static void *
ngx_palloc_block(ngx_pool_t *pool, size_t size)
{
    u_char      *m;
    size_t       psize;
    ngx_pool_t  *p, *new;
    psize = (size_t) (pool->d.end - (u_char *) pool); //计算新开辟的内存池大小,大小和之前的pool一致
    m = ngx_memalign(NGX_POOL_ALIGNMENT, psize, pool->log);
    if (m == NULL) { // 分配失败返回
        return NULL;
    }
    //初始化内存池的一些参数
    new = (ngx_pool_t *) m;
    new->d.end = m + psize;
    new->d.next = NULL;
    new->d.failed = 0;
    //让m指向该块内存ngx_pool_data_t结构体之后数据区起始位置 
    m += sizeof(ngx_pool_data_t);
    m = ngx_align_ptr(m, NGX_ALIGNMENT);
    new->d.last = m + size; // 为需求size分配相应大小的内存并返回
    for (p = pool->current; p->d.next; p = p->d.next) { // 遍历current后的所有内存池的failed值,大于4则current指向下一个内存池
        if (p->d.failed++ > 4) {
            pool->current = p->d.next;
        }
    }
    p->d.next = new;
    return m;
}

分配大内存

static void *
ngx_palloc_large(ngx_pool_t *pool, size_t size)
{
    void *p;
    ngx_uint_t n;
    ngx_pool_large_t *large;
    p = ngx_alloc(size, pool->log); // 调用封装的malloc分配内存
    if (p == NULL) {
        return NULL;
    }
    n = 0;
    // 存在分配过的large块,将large块链接到large链表后
    for (large = pool->large; large; large = large->next) {
        if (large->alloc == NULL) {
            large->alloc = p;
            return p;
        }
        if (n++ > 3) {
            break;
        }
    }
    // 不存在分配过的large块分配一块ngx_pool_large_t结构体来管理large内存
    large = ngx_palloc_small(pool, sizeof(ngx_pool_large_t), 1);
    if (large == NULL) {
        ngx_free(p);
        return NULL;
    }    
    large->alloc = p; //将这块large加入pool中的large链后
    large->next = pool->large;
    pool->large = large;
    return p;
}

内存池释放

typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t;
// cleanup链表结构体
struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt handler; // 函数指针,在c++11引入functional,避免使用函数指针
    void *data; // 先数据清理函数传递参数,即需要清理的资源
    ngx_pool_cleanup_t *next;
};
  • 通过回调函数对资源清理。

8676d3eabdaba689c69cc014ee7db761.png
  • 从图中可以看出,cleanup形成一个环形链表。
  • handler关键字为函数指针,当挂载一个资源到内存池上时候,也会注册一个清理资源函数到handler上。当需要清理该内存时,调用该函数。
  • 注意,nginx是面向连接设计的。并没有对用户提供释放内存的接口。Nginx内存池分为不同等级,由进程,connection,request不同的内存池。首先一个工作进程会创建一个内存池。当有新的连接到来,在worker进程的内存池上创建一个内存池;当一个有一个request进来,则在该连接内存池基础上创建一个request内存池。当request结束,则释放整个request内存池。当连接断开,则释放整个连接内存池
  • 总结,Nginx作为为将小块内存申请聚集,然后一起释放。避免频繁申请小内存,降低内存碎片产生。

Reference

STL源码剖析

https://zhuanlan.zhihu.com/p/34725232

https://www.cnblogs.com/qiubole/archive/2008/03/07/1094770.html

https://www.cnblogs.com/xiekeli/archive/2012/10/17/2727432.html

https://www.cnblogs.com/uestcjoel/p/6687785.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值