malloc与free
malloc 分配的内存是未初始化的,其中的字节内容是不确定的(可能是随机值)。
如果内存分配失败,malloc 返回一个空指针 NULL,可以通过检查返回值来判断是否分配成功。
void* malloc (size_t size);
calloc 分配的内存会被初始化为全0。
calloc 在分配失败时会自动抛出错误(异常),可以使用异常处理机制来捕获和处理错误。
void* calloc (size_t num, size_t size);
realloc 当malloc函数或者calloc函数申请的空间或者数组的空间不够大或太大时就可以用realloc函数对空间的大小进行调整。
void* realloc (void* ptr, size_t size);
free 释放由malloc(),calloc(),realloc()函数动态开辟的内存空间,使其可以重新被分配。
void free (void* ptr);
malloc的底层
当开辟的空间小于128K时,调用 brk()函数,malloc的底层实现是系统调用函数brk(),其主要移动指针 _enddata(此时的 _enddata 指的是Linux地址空间中堆段的末尾地址,不是数据段的末尾地址)
当开辟的空间大于128K时,mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。
不同的内存分配器实现不一样,glibc的ptmalloc采用的是隐藏头风格,gemalloc采用其他的实现方式。
malloc分配的内存为一个个chunk,以下是一个典型的 malloc_chunk 结构定义(以 glibc 为例)
struct malloc_chunk {
size_t prev_size; /* 前一个内存块的大小(如果合并的话) */
size_t size; /* 当前内存块的大小,包括边界标记 */
struct malloc_chunk *fd; /* 指向前一个空闲内存块的指针(用于空闲内存列表) */
struct malloc_chunk *bk; /* 指向下一个空闲内存块的指针(用于空闲内存列表) */
};
在进行malloc函数申请内存时,操作系统实际会申请大于malloc要求的长度
prev_size 字段表示前一个内存块的大小(如果当前内存块与前面的内存块合并在一起)。size 字段表示当前内存块的大小,包括边界标记。fd 和 bk 字段分别表示指向前后空闲内存块的指针,用于维护空闲内存列表。
当释放一个内存块时,内存分配器可以检查 prev_size 字段以确定前一个内存块是否为空闲。如果前一个内存块为空闲,内存分配器可以将这两个相邻的空闲内存块合并成一个更大的空闲内存块。这样可以使内存分配更加高效,减少内存碎片。
malloc返回的指针不是指向了Header,而是指向了Payload开始处。
因此空间的大小记录在参数指针指向地址的前面,free的时候通过这个记录即可知道要释放的内存有多大。
总结:
(1)当调用malloc(size)时,它首先计算需要分配的内存块大小,包括用户请求的大小以及内存管理所需的额外空间(例如内存块的管理信息)。
(2)malloc 会遍历一个数据结构(例如空闲链表或空闲块列表),查找合适大小的空闲内存块。
(3)如果找到了合适的内存块,malloc 会将其标记为已分配,并返回一个指向该内存块的指针给用户。
(4)如果没有足够大的空闲内存块可用,malloc 可能需要扩展程序的虚拟内存空间。它通过系统调用(例如 brk 或 mmap)向操作系统请求更多的连续内存空间。
(5)当操作系统提供了更多的内存空间后,malloc 可以从新的空间中分配出合适大小的内存块,并将其标记为已分配。
(6)在内存块被释放时,通过调用 free 函数,malloc 将其标记为未分配,并将该内存块添加到空闲内存块的列表中,以便后续的内存分配可以重复使用它们。
模拟实现
#include <unistd.h> // 包含系统调用相关的头文件
typedef struct Block {
size_t size; // 内存块的大小
struct Block* next; // 指向下一个内存块的指针
} Block;
Block* freeList = NULL; // 空闲链表的头指针
void* malloc(size_t size) {
// 检查参数是否合法
if (size <= 0) {
return NULL;
}
// 计算需要分配的内存大小
size_t blockSize = sizeof(Block) + size;
// 在空闲链表中查找符合要求的内存块
Block* prevBlock = NULL;
Block* currBlock = freeList;
while (currBlock != NULL) {
if (currBlock->size >= blockSize) {
// 找到合适大小的空闲块
if (prevBlock != NULL) {
// 删除这个空闲块
prevBlock->next = currBlock->next;
} else {
// 这个空闲块是链表的头节点
freeList = currBlock->next;
}
// 返回指向内存块的指针
return (void*)(currBlock + 1);
}
prevBlock = currBlock;
currBlock = currBlock->next;
}
// 没有找到可用的内存块,请求更多内存空间
Block* newBlock = sbrk(blockSize);
if (newBlock == (void*)-1) {
return NULL; // 请求失败,返回 NULL
}
// 返回指向新内存块的指针
return (void*)(newBlock + 1);
}
void free(void* ptr) {
// 检查参数是否合法
if (ptr == NULL) {
return;
}
// 获取指向内存块起始位置的指针
Block* block = ((Block*)ptr) - 1;
// 将内存块标记为未分配状态,然后将其添加到空闲链表中
block->next = freeList;
freeList = block;
}
结论:根据 malloc(size_t) 函数的调用,是有可能申请超过机器物理内存大小的内存块
在1G内存的计算机中是有可能malloc(1.2G)的
malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。根据 malloc 函数的作用和原理,应用程序通过 malloc 函数在虚拟地址空间中申请内存,并且与物理内存没有直接的关系。
malloc 返回的是在虚拟地址空间中的地址,而物理内存的分配是由操作系统完成的。
假设需要申请的内存大小为 1.2GB,转换为字节为 2^30 × 1.2 Byte,这个数值仍然在 unsigned int 的表示范围内。
因为 malloc 函数需要一个 unsigned int 类型的参数来指定内存大小。
在当前使用的 Windows 环境中,可申请的最大内存空间通常超过 1.9GB。然而,具体可申请的内存大小受到操作系统版本、程序本身的大小、动态/共享库的使用情况、程序栈的大小等因素的影响。每次运行的结果可能存在差异,因为有些操作系统使用随机地址分布技术,导致进程的堆空间变小。
关键要点结论
(1) malloc分配的不是物理内存,是虚拟内存
现在的操作系统,内存管理通常是基于虚拟内存的,所以应用程序看到的内存地址(虚拟地址)与实际的物理内存地址(物理地址)是不同的。操作系统通过内存管理单元(MMU)来将虚拟地址转换为物理地址。
当应用程序首次访问这块内存时,操作系统发现对应的物理内存尚未分配,它会从可用的物理内存中分配相应的空间,并更新页表项以完成虚拟地址到物理地址的映射。如果这块内存从来没有被访问,那么就不会分配实际的物理内存,节约了内存。
(2) 调用free后,内存不会被操作系统立马回收
当使用free函数释放内存后,这块内存并不会立即被归还给操作系统。相反,这些被释放的内存首先会被内存管理器(如ptmalloc)保存起来,以便后续重用。这样做的主要目的是减少与操作系统的内存交互次数,从而降低系统调用的开销。
内存管理器会使用双链表等方式来管理这些被释放的内存块,当程序再次申请内存时,内存管理器会首先尝试从这些已释放的内存块中找到合适的块返回给程序,而不是直接向操作系统请求新的内存。这种机制有助于减少内存碎片和提高内存使用效率。