内存分配器 (Memory Allocator)

article/2025/10/1 7:27:51

对于大多数开发者而言,系统的内存分配就是一个黑盒子,就是几个API的调用。有你就给我,没有我就想别的办法。来UC前,我就是这样认为的。实际深入进去时,才发现这个领域里也是百家争鸣,非常热闹。有操作系统层面的内存分配器(Memory Allocator),有应用程序层面的,有为实时系统设计的,有为服务程序设计的。但他们的目的却是一样的,平衡内存分配的性能和提高内存使用的效率。


从浏览器开发的角度看,手机内存的增长速度相对于网页内容的增长仍然只是温饱水平,像Android本身就是用内存大户,还有一个Low Memory Killer, 一定要优化内存占用。总体上对策就是两点:一是能不用就不用,代码里可能隐藏着很多不必要内存分配,特别留意那些中间量。二是能少用就少用,特别避免频繁分配,因为那样只会增加内存碎片,到了极端时即使仍有内存可用,也分配不出来了。还有一个选项: 换个内存分配器
这样一是如果内存分配器优良就可以缓解内存碎片,也可以在出现OOM时控制程序的行为,崩与不崩、崩在哪里就可以自己控制了。


最近因为工作原因,涉及到了小内存分配器,所以做了一些粗浅的学习,没有完整的阅读代码,也没有进行透彻的测试,只是写个总结以及相关的文档放在这里,备查。


内存分配的现实问题


首先通常使用的内存分配器,即malloc/free函数并不是系统提供的,而是C标准库提供的。也被称为动态内存分配器。分配器从操作系统拿内存(虚拟内存)时是以页为单位(通常是4KB,调用sbrk或mmap), 然后再自行管理。


上面也提到了,内存分配器面对的是两个核心问题: 效能和性能(或称为吞吐量Throughoutput)。 前者保证随时有内存可用,后者保证服务时间短、不拖后腿。


对于一个系统进程而言,面对OOM(Out Of Memory)问题,排除程序使用内存的Bug外,会有两个原因:
  1.系统真的没有内存可用了。


  2.内存分配浪费了大量空间,虽然有大量零散的可用空间,却无法合并提供出来使用。 前者才是真正的OOM, 后者就是内存碎片(Fragmentation)问题了。


libc里的malloc遇到分配失败时,默认会abort掉进程,也就是崩掉(CRASH)了。如果系统支持mallopt就有机会改变这个行为,可惜Android还没有支持。

浏览器在加载、解析、渲染页面的时候,会分配大量的小对象,看张图就明白了:

    

上图中模轴为对象大小,纵轴则为申请分配的次数。如果内存都以页为单位申请,就简单,也就不需要分配器。就是那些小对象,占用不多,使用频繁,很容易造成页内无法再继续使用的碎片(Internal Fragmentation)。


对于性能,内存分配是次于I/O的一个瓶径。虽然绝大多数情况下都相安无事,但内存分配器有一个重要的指标,即上限(bounded limits)。虽然平均值看起很好,但一旦遇到最坏的情况(wrost case)时,能不能保证性能?特别是多线程下,内存分配、释放的性能常常受到加锁的影响。有些分配器(如ptmalloc)过于考虑性能,而无法使线程间的内存共享,各自占去一块,反而降低了内存使用的效率。


这些问题一直存在,不同的人针对不同的场景设计出了不同的分配器算法(DSA, Dynamic Storage Algorithms, 是以应用的角度来看的),而且几乎每一个都说自己比别人强。比如:
   1. dlmalloc/ptmalloc/ptmallocX C标准库提供的分配器, 也是应用程序默认使用的malloc/free等函数。
   2. tcmalloc 出自Google, WebKit/Chrome中应用。
   3. bmalloc 毕竟Chrome和WebKit越走越远,所以Apple在WebKit最新代码(2014-04)里提供了新的分配器,号称远远超过 TCMalloc, 至少是在性能上。
   4. jemalloc 原本是为FreeBSD开发的,后来Firefox浏览器和FaceBook的服务端都加以应用,它自身也在这些应用中得到了大幅提升。
   5. Hoard 一个专为多线程优化的分配器, 作者是大学教授,有一些独特的技术。Mac OS X中的malloc就有参考其实现进行优化。

*WebKit另外专为Render Object提供了一个所谓的Plain Old Data Areana的类,也算是一个Memory Pool的实现(PODIntervalTree, PODArena)。



核心思想和算法


分配器这么多,其核心思想相似,只是差在算法和metadata存储上。附13提供的论文中有比较全面的总结,可以翻看一下。


内存分配器的核心思想概括起来三条:


1. 基本功能:首先将内存区(Memory Pool)以最小单位(chunk)定义出来,然后区分对象大小分别管理内存,小内存分成若干类(size class),专门用来分配固定大小的内存块,并用一个表管理起来,降低内部碎片(internal fragmentation)。大内存则以页为单位管理, 配合小对象所在的页,降低碎片。设计一个好的存储方案,即metadata的存储,减少对内存的占用。同时优化内存信息的存储,以使对每个size class或大内存区域的访问的性能最优且有上限(bounded limits)。


比如dlmalloc定义的是一个个bins(同size class)来存储不同大小的内存块:

     
2. 回收及预测功能: 当释放内存时,要能够合并小内存为大内存,根据一些条件,该保留的就保留起来,在下次使用时可以快速的响应。不需要保留时,则释放回系统,避免长期占用。


3. 优化多线程下性能问题:针对多线程环境下,每个线程可以独立占有一段内存区间,被称为TLS(Thread Local Storage),这样线程内操作时可以不加锁,提高性能。下图是MSDN上贴出的关于TLS的原理图,可以参考:

        


*另外测试工具也是必不可少,比如tcmalloc的heap profile, jemalloc则结合valgrind。FireFox在移植jemalloc到Android时,特别关掉了TLS,想必是考虑到它对于线程单一应用的副作用。


上面这些思路对于各个分配器而言基本是一致,但具体如何组织size classes, 如果以一个固定步长,必将形成一个巨大且效率低下的表,原因参考第一张图就明白了。很多年前,就有专门的论文对此做了评定(链接)。另外还有如何定位内存块? 如何解决多线程下的false cache line问题? 不同的分配器使用了不同的算法和数据结构来实现。它们所使用的算法统称为DSA, Dynamic Storage Algorithms。


具体的算法实现可以在下面的参考列表中找到对应的文档, 也可以先看附16,文中分别对DSA Algorithms和DSA Operational Model做了描述,概括的很好,会有一个总体的印象。作者将DSA算法分为五类:

  1. Sequential Fit

     是基于一个单向或双向链表管理各个blocks的基础算法,因为和blocks的个数有关,性能比较差。这一类算法包括Fast-Fit, First-Fit, Next-Fit, and Worst-Fit。

  2. Segregated Free List (离散式空闲列表) 

     使用一个数组,每个元素是存储特定大小内存块的链表,它们所代表的大小并不是连续的,所以称为离散。经典的dlmalloc使用的就是这个算法。数据元素,参照上面的图就可以理解了。TLSF算法则是基于此进行了改进。

  3. Buddy System

    这是由一代大师Donald Knuth提出,后续产生许多的改进版本。最大的作用是解决外部碎片(external fragmentation), 详细的算法,参考这篇(浅析Linux内核内存管理之Buddy System)。

  4. Indexed Fit

   以某种数据结构为每个block建立索引,以求可以快速存取。一般以一个二叉树结构实现。比如使用Balanced Tree的Best Fit allocator, 以及基于Cartesian tree 的Stephenson Fast-Fit allocator。这类算法的性能比较高,也比较稳定。

  5. Bitmap Fit

   这类算法只是索引方法不同,使用以位图式字节表示存储单元的状态。它的好处是使用一小块连续的内存,响应性能更好。Half-Fit就属于这类算法。

随着技术演进,现在主流的allocators, 基本上都是综合运用了两类以上的算法。


另外一些基础算法也是相似的,比如以二叉树组织列表的算法,也就是in-place, 笛卡尔树 和red-block的差异。在线程上,则因为实现的不同,会导致内存占用的差异。比如jemalloc在释放时,并不需要在原来的分配线程执行释放,只是被放回到分配线程的free list中去,ptmalloc则必须回到分配线程里执行释放,性能就相对弱一些。 tcmalloc则设计了算法,让一个线程可以从它的邻居那里偷一些空间来(这个过程称为transfer cache),这样可以有效地利用线程间的内存。


优劣势对比

ptmalloc 劣势:多线程下的性能及内存占用(线程间内存无法共享),并且内存用于存储metadata开销较大,在小内存分配上浪费比较多。优势:算是标准实现。

tcmalloc 劣势:因为算法的设计,占用的内存较大。优势:多线程下的性能。参考附6。

jemalloc 优势: 内存碎片率低,多核下性能较tcmalloc更好。参考附17。


时间有限,没有再深入研究,后面有空再补充一下。在实际应用中,还是有一些参数可以调整的,前提是要熟悉其实现,特别是性能评估的方法。


转载请注明出处: http://blog.csdn.net/horkychen


参考


这是我列的最长的参考清单了,前人的确已经做了很多的研究,我对其中内容只是泛读,并不是所有内容都相关,只是觉得有些内容可以相互应证就也列进来了。
1. jemalloc关于使用red-block tree的反思 [链接]
  文章发布于2008年,作者在2009年将其应用于FaceBook时,则是进行了算法上优化。
2. 2011年jemalloc作者在FaceBook应用jemalloc后撰文介绍了jemalloc的核心算法及在Facebook上应用效果。[链接] [早期的论文,有更多的细节]
3. Android碎片化的度量 通过改造ROM做的实验。
4. Hoard Offical [链接]
5. Mac OS上malloc是怎么工作的[链接]
6. 关于WebKit应用tcmalloc的对比[链接]
7. How tcmalloc works[链接] [中文翻译]
8. TCMalloc源代码分析,很不错资料。作者的网站还有其它干货值得一读。[链接]
9. dlmalloc早期的技术文档,讲述了其核心算法。[链接]
10. ptmalloc源码分析,讲的很系统,非常值得一读。[CSDN下载链接]
11. 介绍jemalloc的资料《更好的内存管理-jemalloc》[链接]
12. 替换系统malloc的四种方法 [链接]
13. 介绍针对实时系统进行优化的内存分配算法TLSF,其中对动态分配算法(DSA)做了总结。[链接]
14. 维基百科上关于Thread Local Storage的说明, 也许你能感受到技术的相通性。[链接]
15. 针对实时系统进行各种分配算法的对比,可以结合13一起看。[链接]
16. ptmalloc,tcmalloc和jemalloc内存分配策略研究。[链接]
17. Firefox3使用jemalloc后的总结,可以看到Firefox优化的思路。[链接] [Firefox使用的源代码]
18. Chromimum Project: Out of memory handling, 里面有不错的观点。 [链接]


http://chatgpt.dhexx.cn/article/sAG1n1sn.shtml

相关文章

内存分配器

内存分配器(Memory Allocator)负责内存分配与管理。内存分配器是所有容器的基础,它总是隐藏在容器背后工作,因此内存分配器对实现者非常重要,而对应用者几乎可以忽略。内存分配器分为两级,第一级分配器直接…

小笔记——内存控制器

内存控制器 what is memory controller? 内存控制器是一个用于管理与规划从内存到CPU间传输速度的总线电路控制器。 工作方式 内存控制器控制着必要的逻辑读写DRAM,每隔一段时间刷新动态随机存取存储器(DRAM)的内容。进行读取和写入动作时&…

DDR内存控制器

DDRDouble Data Rate双倍速率同步动态随机存储器。严格的说DDR应该叫DDR SDRAM,人们习惯称为DDR,其中,SDRAM 是Synchronous Dynamic Random Access Memory的缩写,即同步动态随机存取存储器。而DDR SDRAM是Double Data Rate SDRAM的…

Linux内存控制器(二)

1. memcg_stock_pcp // 每处理器记账缓存一次从内存控制组批量申请32页, 然后把内存控制组的内存使用量加上32页 #define CHARGE_BATCH 32U // 在内存控制组记账(charge)时, 先查看当前处理器的memcg_stock_pcp // 如果memcg_stock_pcp保存的内存控制组(memcg_stock_pcp->c…

DDR控制器

SCL:Self-Calibration Logic,通过寄存器编程方式实现DDR物理层信号校准的逻辑,这部分逻辑全部由硬件实现,软件需要在物理层自动校准之前对寄存器进行初始化。 SDRAM接口宽度在保持相同速率的前提下,可以采用全宽、半宽…

XLINX系列之Zynq-7000系列DDR内存控制器详解

1DDR内存控制器介绍 DDR内存控制器支持DDR2,DDR3,DDR3L和LPDDR2设备,包括三个主要块:AXI存储器端口接口(DDRI),带有交易调度器(DDRC)的核心控制器和具有数字PHY&#xf…

3. 内存控制器与SDRAM

内存控制器(内存接口设备) 地址处于不同的范围,会发出不同的片选引脚,换句话说,SOC外接的不同内存芯片,会有不同的地址范围。 CPU统一编址包括GPIO,各种协议类接口控制器(UART&…

存储控制器

存储控制器是按照一定的时序规则对存储器的访问进行必要控制的设备,包括地址信号、数据信号以及各种命令信号的控制,使主设备(访问存储器的设备)能够根据自己的要求使用存储器上的存储资源。 存储控制器的作用主要就是进行接口的转换,将主设…

内存控制器

1.内存控制器(Memory Controller) 内存控制器(Memory Controller)是计算机系统内部控制内存并且通过内存控制器使内存与CPU之间交换数据的重要组成部分。内存控制器决定了计算机系统所能使用的最大内存容量、内存BANK数、内存类型…

Java并发包中常用类小结(一)

Java并发包中常用类小结(一) 从JDK1.5以后,Java为我们引入了一个并发包,用于解决实际开发中经常用到的并发问题,那我们今天就来简单看一下相关的一些常见类的使用情况。 1、ConcurrentHashMap ConcurrentHashMap其实就是线程安全版本的has…

java并发包源码分析

java并发包之AbstractQueuedSynchronizer源码分析 分析并发包首先要了解AbstractQueuedSynchronizer(AQS),因为AQS是并发包的基础工具类。本文从ReentrantLock的公平锁出发,分析AbstractQueuedSynchronizer的工作过程。 lock与u…

【Java进阶】Java并发包提供了哪些并发工具类?

通过前面的学习,我们一起回顾了线程、锁等各种并发编程的基本元素,也逐步涉及了 Java 并发包中的部分内容,相信经过前面的热身,我们能够更快地理解 Java 并发包。 今天我要问你的问题是,Java 并发包提供了哪些并发工具…

java---JUC并发包详解

目录 前言 一、atomic包 AtomicInteger类 AtomicReference类 AtomicStampedReference类 二、locks包 接口 Condition Lock ReadWriteLock 实现类 ReentrantLock类 ReentrantReadWriteLock类 三、CountDownLatch 四、Semaphore(信号量) 总结 前言 JUC是java.u…

Java多线程并发编程--Java并发包(JUC)

Java多线程并发–Java并发包(JUC) 前言 前一篇文章中,笔者已经介绍了Java多线程的一些基础知识,但是想要成为一名中高级Java程序员还必须懂得Java并发包(JUC)的知识点,而且JUC现在也是面试中必…

接口测试--apipost如何自定义header中的content-type

使用apipost进行接口测试的时候,有时候会用到一些自定义或者不常见的content-type格式,这个时候就要手动在header头部自定义content-type。 这里我们自定义一个content-type,格式为application/octet-stream 然后body选择的为form-data&…

Type-c引脚定义

Type-c口是什么口,有什么作用? Type-c口在大家的视野中或许比较陌生,但是生活中处处离不开Type-c口的存在。手机,电脑,音箱,小家电,无人机…等等,都存在Type-c接口。 Type-c只是一种…

TYPE-C12PIN接口电路

16PIN封装只有12个PIN脚,所有16/12PIN其实是一种规格 特点,正反插 VBS-VCC A5,B5接电阻拉地 A7,B7串联接电阻成为DN A6,B6串联接电阻成为DP GND拉地 外壳拉地 USB接口介绍