c++内存管理

内存管理是c++和其他OOP(面向对象)语言的根本区别,熟知c++的内存管理至关重要

1.分配方式简介

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

  • 栈,函数内局部变量,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 堆,malloc分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个free。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 自由存储区,就是那些由new等分配的内存块,他和堆是十分相似的,不过它是用delete来结束自己的生命的,自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。
  • 全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

1.1 堆栈区别

主要的区别由以下几点:

  1. 管理方式不同。栈系统管理,堆程序员控制,容器产生内存泄漏(memory leak)
  2. 空间大小不同。堆大,栈小。栈可以 设置
  3. 能否产生碎片不同。频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。栈是先进后出,内存永远是连续的
  4. 生长方向不同。堆向上生长(内存地址增加的),栈向下生长(向着内存地址减小的方向增长)
  5. 分配方式不同。堆:动态分配,没有静态分配。栈:静态分配,动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
  6. 分配效率不同。栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

1.2 常见的内存错误及其对策

  • 内存分配未成功,却使用了它。应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
  • 内存分配虽然成功,但是尚未初始化就引用它。赋初值
  • 内存分配成功并且已经初始化,但操作越过了内存的边界。测试覆盖
  • 忘记了释放内存,造成内存泄露。内存泄漏检测工具
  • 释放了内存却继续使用它。用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”

1.3 有了malloc/free为什么还要new/delete?

malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

new/delete:对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数

以下示例展示了malloc和new对于构造函数的是否触发

#include <iostream>

using namespace std;

class A {
public:
    A() {        cout << "construct" << endl;    }
    virtual ~A() {        cout << "destruct" << endl;    }
    void print() {        cout << "hello world" << endl;    }
};

int main()
{    
    {
        A a;
        a.print();
    }
    cout << "--stack--" << endl;
    {
        A* b = new A;
        b->print();
        delete b;
        b = nullptr;
    }
    cout << "--heap with new--" << endl;
    {
        A* c = (A*)malloc(sizeof(A));
        c->print();
        free(c);
        c = nullptr;
    }
    cout << "--heap with malloc--" << endl;
    return 0;
}

输出

construct
hello world
destruct
--stack--
construct
hello world
destruct
--heap with new--
hello world
--heap with malloc--

1.4 内存耗尽怎么办

  • 判断指针是否为NULL,如果是则马上用return语句终止本函数
  • 判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行
  • 为new和malloc设置异常处理函数。例如Visual C++可以用_set_new_hander函数为new设置用户自己定义的异常处理函数,也可以让malloc享用与new相同的异常处理函数。详细内容请参考C++使用手册。

2. 内存管理升级 tcmalloc

tcmalloc就是一个内存分配器,管理堆内存,主要影响malloc和free,用于降低频繁分配、释放内存造成的性能损耗,并且有效地控制内存碎片。glibc中的内存分配器是ptmalloc2,tcmalloc号称要比它快

2.1 优化点

  • tcmalloc特别对多线程做了优化,对于小对象的分配基本上是不存在锁竞争,而大对象使用了细粒度、高效的自旋锁(spinlock)
  • 分配给线程的本地缓存,在长时间空闲的情况下会被回收,供其他线程使用,这样提高了在多线程情况下的内存利用率,不会浪费内存,而这一点ptmalloc2是做不到的
  • tcmalloc为每个线程分配了一个线程局部的cache,线程需要的小对象都是在其cache中分配的,由于是thread local的,所以基本上是无锁操作(在cache不够,需要增加内存时,会加锁)
  • tcmalloc维护了进程级别的cache,所有的大对象都在这个cache中分配,由于多个线程的大对象的分配都从这个cache进行,所以必须加锁访问

在实际的程序中,小对象分配的频率要远远高于大对象,通过这种方式(小对象无锁分配,大对象加锁分配)可以提升整体性能。

线程级别cache和进程级别cache实际上就是一个多级的空闲块列表(Free List)。一个Free List以大小为k bytes倍数的空闲块进行分配,包含n个链表,每个链表存放大小为nk bytes的空闲块。在tcmalloc中,<=32KB的对象被称作是小对象,>32KB的是大对象。在小对象中,<=1024bytes的对象以8n bytes分配,1025<size<=32KB的对象以128n bytes大小分配,比如:要分配20bytes则返回的空闲块大小是24bytes的,这样在<=1024的情况下最多浪费7bytes,>1025则浪费127bytes。而大对象是以页大小4KB进行对齐的,最多会浪费4KB - 1 bytes。

FreeList
FreeList

 

tcmalloc流程

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值