malloc底层实现以及和new的比较

news/2024/10/21 14:45:21

背景:

前几天去面试,被问到了一个问题:“malloc的底层实现是怎样的? 怎样防止内存碎片?” 当时答的不够好,现在再整理一下。

(本文档通过收集整理网上博客而来。先挖个坑,等有时间了去看一下《深入理解操作系统》的第九章虚拟内存,再重新整理一篇)

内存布局

Linux中每个进程都有自己的虚拟地址空间,通常会划分为几个区域。如下:

+--------------------------------------------------+
|           命令行参数和环境变量                      |
+--------------------------------------------------+
|           栈   局部变量,参数等。后进先出             |  (从高地址向低地址增长)
+--------------------------------------------------+
|           堆   动态内存分配 malloc calloc分配出来的  |  (从低地址向高地址增长)
+--------------------------------------------------+
|       BSS 段   未初始化的全局变量和静态变量           |
+--------------------------------------------------+
|   初始化数据段   已初始化的全局变量和静态变量           |
+--------------------------------------------------+
|       文本段    代码段,包含程序机器指令,只读         |
+--------------------------------------------------+

补充一点:

栈:速度快,不用程序员释放,空间小,容易栈溢出,有的系统只有8M。

堆:速度慢一点,空间大,要自己释放,管理起来难度大,容易内存泄漏。new 和 malloc都是操作的堆区。

 

malloc底层实现

小内存分配(通常小于128KB):

通常使用内存池(通过sbrk系统调用)来管理,先从内存池中查找一个空闲的块,将其标记为已分配,并返回指针。

DEFAULT_MMAP_THRESHOLD一般为128k,可通过mallopt进行设置。

sbrk通过移动堆指针来实现分配。

大内存分配:

通常使用mmap系统调用来直接分配内存。mmap将文件或匿名内存映射到进程的地址空间,返回一个指向映射区域的指针。

 Free List():

空闲列表(free list),malloc实现分配内存时会利用空闲链表来操作。

本质是一个链表,用于存储已经释放尚未分配的内存块,当需要分配时,优先从空闲链表中选择合适的内存卡。数据结构大致如下:

struct FreeBlock {size_t size;          // 块的大小bool bIsAvalib;  // 是否使用struct FreeBlock *next; // 指向下一个空闲块的指针
};

如下图: 来源于:https://www.jianshu.com/p/2fedeacfa797

 

大致流程:

初始化时:空闲列表通常为空。会向操作系统申请一块初始内存,作为第一个空闲块插入空闲列表中。

分配时:查找合适的空闲块,返回指针。(这里有不同的分配策略)

释放时:将释放的内存插入空闲列表中。并合并相邻的空闲块(为了避免出现内存碎片)。

分配策略:

大致有下列几种分配策略,配合内存管理模块,实际上会比较复杂,可能是几种方式组合使用。

1)从第一个空闲块开始找,找到空间够的。

2)从上一次分配的空闲块开始找个空间够的。

3)找个大小最接近的。

4)实际上可能会维护多个不同大小的空闲列表,根据malloc要申请的大小去使用不同的空闲列表。

 

关于free:

1) 调用free后,会自动合并相邻的空闲块,为避免出现内存碎片。

2)为什么free可以不用传大小?

因为每次malloc后,申请的内存会比实际使用的大一点,在用户使用的内存前面会存放内存块头部信息,里面会存着本内存块的大小。free时根据该大小来正确回收资源。

 如下图:是用vs2017测试的。

 

内存碎片:

定义: 

在频繁的内存分配和释放的过程中,可能会导致内存碎片。大致分两类:

内部碎片(Internal Fragmentation):已经被分配出去,结果无法使用的内存空间。比如用户申请了45字节,内存分配必须是4字节对齐的,系统可能会分配48个字节给用户。多出来的3个字节就成为了内部碎片。

外部碎片(External Fragmentation):由于频繁分配释放,导致内存中出现许多小的,不连续的内存区域。这些区域可能总和能满足新内存请求,但是由于不连续而无法使用。

影响:

性能下降:碎片过多,会导致再分配时需要更多的时间来查找合适的内存块。

内存利用率低:

系统稳定性问题:引发程序崩溃或系统挂起。

分配失败:用户无法分配足够的内存空间,影响用户程序运行。

解决策略:

1)内存池(Memory Pool):预先分配一块打内存,并划分为小块。每次申请时从内存池中获取。

2)紧凑(Compaction):移动内存中的数据块,将分散的空闲块合并。需要额外的开销。

3)使用不同的内存分配算法:

4)内存对齐和预取:确保内存分配是对齐的,减少内部碎片。

实际使用: 

操作系统提供了多种内存管理机制,Linux下如:slab分配器,伙伴系统等。 win下有Heap Manager等,都可以用于优化内存的分配释放。

 

new和delete

1.new和delete是运算符(malloc free是函数),理论上可以被重载,实现自己的new 运算符。

2.malloc失败了返回空指针,new失败了抛异常(没有被重载的情况下)。、

3.new 和 delete配套使用。  new数组 和 delete[]配套使用。

4.new不仅会分配内存,还会调用构造函数。

 

两个小开脑洞的问题:

问题1:malloc出来的指针,用delete释放?

问题2:new出来的指针,用free释放?

先上结论:

1)malloc出来的空间,可以通过delete回收。

2)new出来的空间,可以通过free回收。(如果new出来的是数组,无法用free回收,会引起崩溃)

3)上述理论可行,但是不建议这么做。还是要配套使用,或者使用智能指针管理变量。

上代码:

mallocTest.cpp

#include <iostream>struct AAA
{AAA(){printf("AAA() ... \n");}~AAA(){printf("~AAA() ... \n");}int index;float fdata;int arrData[1024 * 1024 * 100];
};void mallocTest()
{printf("mallocTest  +++++ \n");int nnn = 2;auto pp = (AAA*)malloc(sizeof(AAA)*nnn);pp->index = 555;for(int i = 0; i < nnn; i++){pp[i].index = i;int len = sizeof(pp[i].arrData) / sizeof(int);for(int j = 0; j < len; j++){pp[i].arrData[j] = i * j *j;}}printf("mallocTest  new end. getchar free \n");getchar();delete pp;//free(pp);printf("mallocTest  free end. getchar quit \n");getchar();
}void newTest()
{printf("newTest  +++++ \n");auto pp = new AAA();pp->index = 555;int len = sizeof(pp[0].arrData) / sizeof(int);for(int j = 0; j < len; j++){pp[0].arrData[j] = 1 * j *j;}printf("newTest  new end. getchar free \n");getchar();free(pp);printf("newTest  free end. getchar quit \n");getchar();
}int main()
{printf("main  +++++ \n");//newTest();
    mallocTest();return 0;
}

先执行mallocTest():

同时打开htop查看系统占用情况,按下回车,再次查看系统占用情况。

malloc分配完内存情况:

delete后内存情况:

 

再执行newtest:

new之后(free之前):

free之后:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.ryyt.cn/news/74187.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈,一经查实,立即删除!

相关文章

017 事件总结

如果要达成多个效果,如既要阻止冒泡,又要阻止跳转.修饰符可以连续写

Java 当中使用 “google.zxing ”开源项目 和 “github 的 qrcode-plugin” 开源项目 生成二维码

Java 当中使用 “google.zxing ”开源项目 和 “github 的 qrcode-plugin” 开源项目 生成二维码 @目录Java 当中使用 “google.zxing ”开源项目 和 “github 的 qrcode-plugin” 开源项目 生成二维码1. Java当中使用 “google.zxing ” 开源项目生成二维码1.1 准备工作1.2 生…

第5课 GIT版本控制器

1、版本控制: 一种在开发过程用于管理我们对文件,目录或工程等内容的修改历史,方便查看历史记录,备份以恢复以前的版本软件工程技术。 2、仓库: 受版本控制所有文件修订历史的贡献数据或文件 3、工作空间: 本地硬盘或linux用户账户上编辑的文件副本 4、工作树/工作区: 工…

CMDB平台(基础篇):聊聊CMDB和监控的关系

CMDB与IT监控之间存在着紧密且重要的关系,它们共同构成了IT运维管理的两大支柱。我们将从几个方面详细探讨它们之间的关系: 一、定义与功能 CMDB: 定义:CMDB是一个集中存储系统,它包含了与IT基础设施相关的所有配置信息,这些信息涵盖了硬件、软件、网络设备、服务器、应…

在使用的CSS渲染的网页上进行编辑时光标乱跳,导致编辑不正常

前一阵子发现notion网页版突然抽风,输入时光标总是会莫名移到最前,起初认为是notion的问题,但是搜索无果。遂下载了notion客户端。后来在使用chatGPT时,也发现了此问题,这说明该问题与notion很大概率是无关的。于是我将目标转移到了浏览器上,果然,经过测试是我安装的一款…

如何使用WebSockets

使用WebSockets你需要遵循以下步骤:一、理解WebSockets与传统HTTP的差异;二、选择合适的库和框架;三、建立WebSocket服务器;四、构建WebSocket客户端;五、确保连接的安全性。在开始使用WebSockets前,我们首先需要明白其背后的设计理念和技术特点。一、理解WebSockets与传…

count(*)、count(1)哪个更快?面试必问:通宵整理的十道经典MySQL必问面试题Jv

合集 - 面渣逆袭(10)1.阿里面试:Java开发中,应如何避免OOM02-212.美团面试:Kafka如何处理百万级消息队列?02-203.Java异常处理的20个最佳实践:告别系统崩溃02-224.揭秘一线大厂Redis面试高频考点(3万字长文、吐血整理)02-235.美团面试:说说OOM三大场景和解决方案? (绝…

如何使用Python调用API数据

为什么使用Python调用API数据? 简洁的语法:Python的简洁性使得编写API调用代码变得直观易懂。 强大的库支持:Python拥有如requests这样的库,极大地简化了HTTP请求的发送和响应的处理。 数据处理能力:Python的数据处理库,如Pandas,使得数据的分析和处理变得简单。 社区支…