探秘 `malloc`:超量分配背后的内存分配器难题与技术权衡!

发布时间:2026/7/24 1:41:11

探秘 `malloc`:超量分配背后的内存分配器难题与技术权衡!
深入探究为何 malloc 超量分配实现内存分配器竟有这么多难题2026 年 7 月 20 日阅读时长 11 分钟。几乎所有人在编写“安全”的 C 程序时都见过这样一行代码void *p malloc(13);我们知道这行代码的作用至少知道其目的请求 13 字节获得 13 字节。出于好奇在某个周五开始研究这个问题时我也是这么想的。然而在还没写一行代码的 30 分钟后我就意识到这种情况几乎不会发生。这简单的一行代码在底层其实涉及到了许多有趣的计算和内存分配操作。例如当请求 13 字节时实际预留的内存结构如下-------------------------------------------- | 头部 | 回指指针 | 填充 | 用户内存 | --------------------------------------------用户内存是我们唯一会使用的部分*p 返回的就是这块内存的起始地址。而其他部分只是存在那里并不会被使用。这是为什么呢我也有同样的疑问。与其空谈理论不如来构建一个内存分配器和释放器。最简单的内存分配器最简单的内存分配器甚至没有 free() 函数它被称为“指针偏移分配器”bump allocator。你需要预先给它一大块内存一个内存池arena每次分配只需将指针向前移动即可。typedef struct { uint8_t *cursor; uint8_t *limit; } Arena;cursor 是下一次分配的起始位置limit 是内存池的结束位置。这就是整个设计思路。void *bump_alloc(Arena *a, size_t size) { if (a-cursor size a-limit) return NULL; // 内存不足 void *ptr a-cursor; a-cursor size; return ptr; }就是这么简单。指针只能一直向前移动这是最快的分配器。但从功能上来说它对于长期运行的程序没什么用因为没有 free() 函数。你只能一次性释放整个内存池而不能释放其中的单个对象。这就引出了一个明显的问题如果不能释放单个对象那我怎么收回那 13 字节呢事实证明以这种设计是做不到的。要实现单个对象的 free() 功能分配器需要记录每次分配的相关信息。从这时起元数据就变得不可或缺了。为什么需要元数据free(ptr) 函数只接受一个参数即一个指针。如果分配器的任务是回收这块内存并使其可再次使用它就必须回答一个调用者从未告知的问题这次分配的内存有多大它实际从哪里开始唯一的解决办法是将答案存储在分配器之后能找到的地方而且只能通过 ptr 本身来查找。最明显的位置就是在分配给用户的内存之前。typedef struct { size_t size; } Header;所以现在 alloc() 函数不再只是返回原始内存而是这样操作[ 头部 ][ 用户内存 ] ^ 返回给调用者而 free(ptr) 函数则需要向后查找Header *h (Header *)ptr - 1; // 现在 h-size 告诉我们这个内存块有多大这是一种简单直接的方法但还有更多需要考虑的地方。简单头部与内存对齐的问题并非所有类型的数据都能存放在任意地址这就是所谓的“内存对齐”alignment。char 类型的数据对内存位置没有要求对齐值为 0。double 类型的数据在大多数平台上要求其起始地址是 8 的倍数对齐值为 8。不同类型的数据还有更多不同的对齐要求。虽然在使用不同对齐方式时程序编译不会受到严格限制但这可能会影响性能甚至导致程序意外崩溃。所以alloc() 函数不仅要返回一个指针还需要返回一个针对调用者要存储的数据类型正确“对齐”的指针。这意味着头部结束位置和用户指针实际起始位置之间可能会有一个间隙而且这个间隙的大小每次都不同取决于所请求的对齐方式。这个间隙就是所谓的“填充”padding。[ 头部 ][ ...可变填充... ][ 用户内存 ]这就是 header (Header *)ptr - 1 方法失效的原因。这种减法操作假设从 ptr 到头部的距离是固定的但填充的大小是可变的每次分配都不同。free() 函数无法知道针对这个特定指针插入了多少填充字节因此无法可靠地向后查找头部信息。回指指针的作用如果到头部的距离不固定那就不要依赖距离而是在一个固定位置存储一个指向头部的“回指指针”Back Pointer。[ 头部 ][ ...可变填充... ][ 回指指针 ][ 用户内存 ] ^ 始终在用户内存前 sizeof(void*) 字节处 无论前面有多少填充字节现在free() 函数不需要知道或重新计算填充字节的数量只需这样操作Header *h *(Header **)((uint8_t *)ptr - sizeof(void*));这个位置始终在用户指针前 sizeof(void*) 字节处无论对齐方式如何。头部可以位于任何位置距离多远都没关系回指指针只需指向它即可。到底是哪个指针需要对齐等等需要对齐的不是“内存池的指针”而是“用户指针”也就是实际返回给调用者的指针。内存池指针可以位于任意位置因为没有人会直接对其进行解引用操作。所以实际的计算过程如下候选地址 头部 sizeof(Header) sizeof(void*) 填充字节 对候选地址进行对齐操作 用户指针 候选地址 填充字节填充字节的数量也会存储在头部主要是因为这在后续的调试和测试中很有用你可以通过检查 user_ptr - 填充字节 是否正好回到候选地址来进行合理性检查。内部碎片问题当实现了头部、回指指针和内存对齐功能后看着内存布局一个明显的问题出现了元数据和实际对象之间的那些填充字节在整个分配周期内都闲置着为什么分配器不能重新使用它们呢因为分配的内存必须是一个连续的块。调用者被承诺从 user_ptr 开始有 N 个连续的字节如果说“这是你的内存但中间这 3 个字节属于其他东西”就会破坏分配的基本规则。所以这些字节虽然被分配了即它们属于这个内存块不能再分配给其他对象但实际上从未被使用过。这就是“内部碎片”internal fragmentation它是由于对齐要求而产生的浪费存在于每个需要填充的分配操作中。如何释放已分配的内存在大多数语言如 Python、Golang中解释器或编译器内部有一个称为“垃圾回收器”garbage collector的组件它会检查内存的生命周期如果内存超出作用域或不再使用就会清除其占用的空间。但在像 C 这样的老语言中必须手动释放已分配的内存这有一套规则需要单独写一篇博客来详细介绍。有了头部和回指指针后free() 函数终于可以实现了。但“指针偏移分配器”仍然无法做到这一点因为它没有内存重用的概念。所以设计需要改变不再使用只能向前移动的指针而是需要一个“空闲列表”free list这是一个由已释放的内存块组成的链表这些内存块可以再次被分配。typedef struct FreeNode { size_t size; struct FreeNode *next; } FreeNode;free(ptr) 函数通过回指指针找到头部然后将其添加到空闲列表的头部void push_free(FreeNode *node) { node-next free_list; free_list node; }现在alloc() 函数有两条路径遍历空闲列表查找足够大的内存块search_free。如果没有合适的内存块就回到原来移动指针的分配方式。“首次适应”first-fit是最简单的搜索策略即选择第一个足够大的内存块不用想得太复杂。更复杂的策略如“最佳适应”、按大小类划分的隔离列表会陷入更深的技术细节但“首次适应”足以证明这个概念是可行的。内存块分割我实现的第一个版本的内存分配器有一个很傻的问题。如果它找到一个足够大的空闲内存块即使只需要其中一小部分也会把整个内存块都分配出去。请求8 字节 空闲内存块 ------------------------------------------- | 200 字节 | ------------------------------------------- 结果 ------------------------------------------- | 所有 200 字节都被占用 | -------------------------------------------程序只使用了前几个字节但剩下的 192 字节在这个分配被释放之前无法被其他对象使用这就造成了内存浪费。解决办法是“分割”splitting。不再把整个内存块都分配出去而是把它切成两块。分割前 ------------------------------------------- | 空闲内存块 | ------------------------------------------- 分割后 ------------------------------------------ | 已分配内存 | 仍空闲的内存 | ------------------------------------------前面的部分成为分配的内存剩下的部分直接放回空闲列表等待下一个请求。说起来容易做起来难。我第一次实现时不小心把第二个内存块的起始位置弄错了。我忘记了第一个内存块的元数据也会占用空间没有在第一个内存块完全结束后再创建第二个内存块而是提前了一点。这个 bug 花了一段时间才找到因为分配器并没有立即崩溃而是在悄悄损坏自己。另一个经验是要知道“何时不进行分割”。假设分割后只剩下很少的字节这其实不能算一个新的空闲内存块只是一些零碎的空间。如果剩余的空间甚至不足以描述自身那就没有必要保留它。所以有时候浪费几个字节比创建一个永远无法再使用的小内存块要好。我把这个限制称为 min_split_size。内存块合并把碎片重新组合起来内存块分割会产生更小的内存块但最终会出现相反的问题。想象一下把一张纸剪成很多小碎片即使你总的纸张数量足够但可能没有一块碎片大到能满足你的需求。内存也是一样的道理。假设我分配了两个内存块---------------- | a | b | ----------------后来这两个内存块都被释放了---------------- | 空闲 a | 空闲 b | ----------------它们在内存中是相邻的合起来足够进行一次更大的分配。但我的分配器仍然认为它们是两个独立的内存块。所以如果有人请求一个比单个内存块都大的内存需要32 字节 可用内存 16 字节 16 字节分配会失败尽管总的内存是足够的。解决办法是“合并”coalescing这只是一个专业术语意思是把相邻的空闲内存块重新组合起来。合并前 ---------------- | 空闲 a | 空闲 b | ---------------- 合并后 ----------------- | 一个大内存块 | -----------------找到右边的内存块很容易因为每个内存块都知道自己的大小也就知道前一个内存块的结束位置但它们都不知道前一个内存块的起始位置。解决办法出奇的简单把大小信息写两遍一遍写在内存块的开头一遍写在结尾。------------------------- | 头部 | 数据 | 尾部 | -------------------------现在站在任何一个内存块的起始位置我都可以查看前一个位置从其尾部读取前一个内存块的大小然后直接跳到它的头部。只有这样我才能在两个方向上合并内存块。这是那种解决一个问题却又引入更多元数据的情况之一这和我一开始想让分配器“更精简”的预期完全相反。总结最初只是想“自己实现一个 malloc”但很快就陷入了一系列的权衡之中。引入内存对齐带来了填充字节的问题从此陷入了一个又一个的技术难题每个解决方案在解决一个问题的同时又会引入新的问题。这可能是我从这个项目中学到的最重要的一点。…更多有趣的内容以下内容没有包含在正文里但可能是我从中学到最多的 bug。bug #1“等等……为什么程序会崩溃”实现内存块分割后一切看起来都正常。然后我用 -fsanitizeundefined 选项编译程序出现了如下错误运行时错误对类型 struct FreeNode 进行成员访问时地址 0x61d000000173 未对齐该类型需要 8 字节对齐我首先想到的是“这没道理啊我已经对每个分配都进行对齐操作了。”结果发现我对齐的是“用户指针”而不是“剩余内存块的头部”。当一个大内存块被分割时我只是在剩余字节的起始位置直接放置一个 FreeNode 结构体。FreeNode *remainder (FreeNode *)((uint8_t *)header sizeof(FreeNode) needed);有时这个地址不能被 8 整除有时又可以这就解释了为什么这个 bug 只是偶尔出现。解决办法很简单在将剩余内存块视为 FreeNode 之前先进行对齐操作。之后这个检查工具安静了大约五分钟。bug #2400 次操作后出现内存损坏这个 bug 是最严重的。我的压力测试进行了数千次随机的内存分配和释放操作。一切看起来都正常但在数百次操作后一个从未被触及的内存分配突然损坏了。原因其实很简单我只在释放内存块或分割内存块时写入尾部信息普通的内存分配操作从未写入尾部信息。所以很久之后当进行合并操作时程序会读取尾部位置的随机字节这些随机字节被当作随机的内存块大小进而得到一个随机的指针。解决办法是每次内存块大小改变时都要写入尾部信息一定要这样做

相关新闻

电力系统故障分类:小波变换与图注意力网络融合模型

电力系统故障分类:小波变换与图注意力网络融合模型

2026/7/24 1:41:11

1. 项目背景与核心挑战电力系统故障分类是智能电网运行维护中的关键环节。同步相量测量单元(PMU)以每秒30-120帧的频率采集电网各节点的电压电流信号,这些毫秒级数据流中蕴含着系统运行状态的完整信息。当发生发电机跳闸、负荷切除或短路故障时,信号波形…

Java生态下的企业级AI开发实践与优化

Java生态下的企业级AI开发实践与优化

2026/7/24 1:31:11

1. 为什么Java团队需要关注AI开发?在传统印象中,AI开发似乎是Python的专属领域,但实际情况正在发生变化。作为企业级应用开发的主力军,Java生态正在快速拥抱AI技术栈。我最近主导的几个企业级AI项目落地案例表明,Java团…

BitNet:1-bit量化大模型在CPU上的高效部署与实践

BitNet:1-bit量化大模型在CPU上的高效部署与实践

2026/7/24 1:31:11

1. BitNet:微软推出的轻量化大模型革命去年第一次听说BitNet时,我正在为一台老旧的开发机发愁——这台只有4核CPU的机器跑不动任何主流的大语言模型。直到看到微软研究院的论文,才发现原来在CPU上跑大模型并非天方夜谭。BitNet通过1-bit量化技…

联想AI产品组合:边缘计算与情境感知技术解析

联想AI产品组合:边缘计算与情境感知技术解析

2026/7/24 2:31:15

1. 联想AI产品组合的技术革新解析在2026年国际消费电子展的全球创新科技大会上,联想展示了一套突破性的AI产品组合,这套系统最显著的特点是实现了"个性化、感知型和主动式"三大技术特性的深度融合。作为长期跟踪消费电子行业的技术观察者&…

数字孪生技术在智慧营房管理中的应用与实践

数字孪生技术在智慧营房管理中的应用与实践

2026/7/24 2:31:15

1. 项目概述:当营房管理遇上数字孪生去年参与某部智慧营房改造项目时,我们遇到个棘手问题:传统二维平面管理系统无法直观反映营区真实状态,值班人员需要反复比对监控画面和平面图纸才能定位异常情况。这正是"智慧营房数字孪生…

LLM垃圾评论检测:技术原理与社区防护实战指南

LLM垃圾评论检测:技术原理与社区防护实战指南

2026/7/24 2:31:15

最近在 Hacker News 上发布了一个 Show HN 项目后,我发现了一个令人不安的现象:评论区里那些看似热情洋溢的"用户反馈",仔细一看竟然大多是 LLM(大语言模型)生成的垃圾评论。这些评论用词华丽、语法完美&…

TPS536C7控制器Turbo模式与热平衡管理机制深度解析

TPS536C7控制器Turbo模式与热平衡管理机制深度解析

2026/7/24 2:31:15

1. 项目概述:TPS536C7控制器深度解析在数据中心、AI服务器和高端计算平台的心脏地带,多相降压控制器扮演着至关重要的角色。它不仅仅是把12V输入转换成CPU或GPU所需的1V以下低电压那么简单,更是一个集成了精密电流控制、动态功率管理和智能热…

基于JAX的Tunix智能体后训练:高吞吐优化与实战指南

基于JAX的Tunix智能体后训练:高吞吐优化与实战指南

2026/7/24 2:31:15

最近在智能体训练领域,Google 推出了一个备受关注的新工具 Tunix,它基于 JAX 框架专门针对高吞吐场景下的智能体后训练需求进行了优化。对于从事强化学习、多智能体系统开发的工程师来说,传统训练方法在数据并行处理和计算效率上往往遇到瓶颈…

AI模型路由器:企业级LLM智能调度与成本优化实战方案

AI模型路由器:企业级LLM智能调度与成本优化实战方案

2026/7/24 2:21:14

在企业级AI应用开发中,大语言模型(LLM)调用成本的控制一直是技术团队面临的现实挑战。近期Ramp公司公开的AI模型路由方案,通过智能调度多个LLM服务商实现30%成本优化的案例,为这个问题提供了值得借鉴的工程思路。本文将…

微服务进阶:服务网格与Istio

微服务进阶:服务网格与Istio

2026/7/23 3:40:08

541|微服务进阶:服务网格与Istio 上篇文章我们聊了微服务的基本概念和拆分方法。 但微服务多了,问题也多了: 服务之间怎么通信? 怎么监控每个服务的调用链路? 熔断、限流、重试怎么做? 安全认证怎么统一? 以前这些都靠SDK库(比如Hystrix、Feign),每个服务都要集成…

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

零售超级终端全域协同:ShareKit 碰一碰商品流转业务落地案例

2026/7/23 4:40:05

一、零售门店全域协同业务背景与行业痛点 1.1 门店超级终端设备矩阵(连锁便利店/商超标准配置) 自助收银Kiosk一体机:顾客结算、自助核销优惠券、商品素材预览;运营折叠平板:店长后台商品上新、图片录入、活动配置、…

噗叽短视频界面分析

噗叽短视频界面分析

2026/7/23 1:54:13

1 和小红书类似,可以采用类似判断方法------------其实他比小红书好判断,因为他没有图片,控件位置几乎是固定的,都不用判断------------2 因为他没有点赞按钮------------而且几乎所有控件位置都是完全一样的,所以我就…

Django毕设项目:基于 Django 的 智能化学生综合素质测评审核系统 校园学生评优评奖综合管理系统(源码+文档,讲解、调试运行,定制等)

Django毕设项目:基于 Django 的 智能化学生综合素质测评审核系统 校园学生评优评奖综合管理系统(源码+文档,讲解、调试运行,定制等)

2026/7/24 0:01:07

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

[具身智能-634]:Python 封装的地平线 VIO 多媒体库:libsrcampy库详解

[具身智能-634]:Python 封装的地平线 VIO 多媒体库:libsrcampy库详解

2026/7/24 0:01:07

srcampy /libsrcampy 名称释义先明确结论: 官方文档没有公布标准化英文全称,是地平线内部项目缩写;行业公认拆解如下:srcampy Source Amplifier Python bindingsrc Source(图像源:MIPI Sensor、视频源&am…

用Highcharts 创建可拖拽三维散点立方体3D图表

用Highcharts 创建可拖拽三维散点立方体3D图表

2026/7/24 0:01:07

该案例基于Highcharts scatter3d 三维散点图实现空间立方体散点可视化,核心特色:三维 X/Y/Z 三轴空间,所有散点分布在 0~10 立方体空间内;散点使用径向渐变实现立体 3D 圆球质感;支持鼠标 / 触屏拖拽画布,…