new vs malloc
C++中有operator new和new expression,两者不一样,operator new跟malloc类似,只用于分配内存,new expression则是常用的new一个对象。后者调用前者分配内存,并运行constructor进行初始化。
同样的,也存在operator delete 和 delete expression。
什么时候用new or malloc:
当需要一个对象的生命周期超过他本身的scope时
- new是C++中的操作符,malloc是C语言的库函数。
- 调用方式:new自动计算所需内存大小,malloc需要指定分配内存大小
- 返回值:new返回对象类型的指针,malloc返回void*指针
- 分配失败:new抛出异常
std::bad_alloc
,malloc返回NULL - 分配区域:malloc在堆上分配内存,new默认在堆上,但是可以重载operator new来自定义内存分配行为,所以不一定是在堆上。
- 实现:malloc先在堆上进行内存分配,如果剩余内存不够,会调用
sbrk
或mmap
系统调用来扩大内存,当所需内存小时调用sbrk
,当所需内存大时调用mmap
,sbrk
是直接移动heap的边界来扩展内存,mmap
是直接在内存映射区分配内存。- malloc背后还维护了一个内存池,每次进行系统调用时,申请的空间是大于我们需要的空间的,free时也不会立刻把内存还给系统。这很容易理解,频繁的系统调用非常影响性能,维护一个内存池更好一些。
- 但是如果是使用mmap得到的内存,free时会立刻还回去。
- 释放:malloc对应free,new对应delete,不能混用。delete会先调用析构函数,再调用operator delete。malloc分配时会在这段内存的头部多分配出来一块头块,用于存放内存管理的信息,包括该部分内存长度,指向上/下一个block的指针等,free会将指针左移到头部根据内存管理信息释放内存,而且也分配和释放时也不一定是完全按照指定size大小分配的,有可能为了对齐会分配为2的倍数。
代码:
struct TempStruct
{
/* data */
int a;
float b;
char c;
TempStruct() : a(0), b(0.0), c('0') {std::cout << "TempStruct" << std::endl;}
~TempStruct() {std::cout << "~TempStruct" << std::endl;}
static void* operator new(size_t size) //operator new可以重载,但是new expression不能重载
{
std::cout << "new" << std::endl;
return malloc(size);
}
static void operator delete(void* p) //operator delete可以重载,但是delete expression不能重载
{
std::cout << "delete" << std::endl;
free(p);
}
};
void* GetValue()
{
void* p = malloc(sizeof(TempStruct)); //当需要该对象的生命周期超出作用域时,才使用malloc/new
TempStruct* p2 = new TempStruct; //new expression
delete p2;
return p;
}
int main()
{
TempStruct* p = static_cast<TempStruct*>(GetValue());
free(p);
try {
// 尝试分配一个非常大的内存块,可能会导致失败
TempStruct* ptr = new TempStruct[10000000000000];
std::cout << "分配成功" << std::endl;
delete[] ptr;
} catch (const std::bad_alloc& e) {
std::cout << "内存分配失败: " << e.what() << std::endl;
}
return 0;
}
扩展:
malloc分配是分配虚拟内存,在没有使用时不一定有与物理内存的映射,这与操作系统的策略有关。在第一次访问时会产生缺页中断,操作系统会介入,查找可用的物理内存页,并将其分配给该虚拟内存页,同时更新页表以建立虚拟内存与物理内存之间的映射关系。
new的出现就是为了满足RCII准则,但是上面我们写的代码还是不满足RCII,如果当我们new/malloc后,还没有等到delete/free时代码就出bug崩了,那就会出现memory leak,好的办法是直接将new或者malloc出来的指针放到智能指针里。
因为new本质是分两步,所以new不是线程安全的,需要人为保证new操作的线程安全。
多线程下的内存分配问题:
对于ptmalloc分配器,多线程时,会分为主分配区和从分配区:
主分配区一个进程只能有一个,其是调用brk,从堆区去分配内存。
从分配区一个进程程可以拥有多个从分配区,其调用mmap从memory mapping区域去分配一个sub-heap
原因:因为数据竞争问题,在一个线程使用堆时,其他线程不能访问,如果把其他线程挂起来,那多线程速度也会受影响,所以ptmalloc就定义了per thread arena,每次只对使用到的arena进行加锁,以实现更细粒度的互斥。性能更好。thread arena不是每个线程一个,而是受核心数量限制的。
PS:
内存分配器有很多不同的实现,上面谈到的ptmalloc是glibc中内置的内存分配器。
还有很多开源的其他实现,例如Google开源了一个tcmalloc,自定义了malloc和C++的operator new
Ref
How does malloc work in a multithreaded environment
Difference between ‘new operator’ and ‘operator new’?
深入理解 glibc malloc:内存分配器实现原理