每日一博 | 虚拟机内存管理之内存分配器
小编:本文由 WebInfra 团队姚忠孝、杨文明、张地编写。意在通过深入剖析常用的内存分配器的关键实现,以理解虚拟机动态内存管理的设计哲学,并为实现虚拟机高效的内存管理提供指引。
- 内部碎片:分配出去的但没有使用到的内存,比如需要 32 字节,分配了 40 字节,多余的 8 字节就是内部碎片。
- 外部碎片:大小不合适导致无法分配出去的内存,比如一直申请 16 字节的内存,但是内存分配器中保存着部分 8 字节的内存,一直分配不出去。
- 检测线性溢出, UAF 等多种内存非法访问异常
- 内存加固(地址随机化, MTE , etc .)
1. dlmalloc [1]
* Key Points ( salient points )
- 从操作系统分配( sbrk , mmap )一大块内存 " Segment " ( N * page_size ),多个 Segment 通过链表相互连接," Segment "可以为不同大小。
- 每次申请内存,从 Segment 中分配一块内存" chunk "的内存地址(如果当前 Segment 不满足分配需求,进入步骤 a. 从操作系统分配 Segment )," chunk "可以为不同大小。
- dlmalloc 中切分出的各中大小的 chunk 需要通过" bin "来统一管理," Bin "用于记录最近释放的可重复使用的块(缓存)。有两种类型的 Bin :" small bin "和“ tee bin ”。Small bins 用于小于 0x100 字节的块。每个 small bin 都包含相同大小的块。" tree bin "用于较大的块,并包含给定大小范围的块。" small bin "被实现为简单的双向链表," tree bin "被实现 chunk 大小为 key 的 tries 树。
小内存( Small < 0x100 bytes )
大内存( Large )
超大内存( Huge ) > 64 kb
* Weakness
- dlmalloc 不是线程安全的,因此 bionic 已经切换到更现代的堆实现方案
- 典型的 Buddy memory allocation 算法,这种算法能够有效减少外部碎片,但内部碎片严重。
2. jemalloc ( Android 5.0.0 ~ )
* Key Points ( salient points )
- 调用 mmap 从操作系统分配一整块大内存 " Chunk ", Chunk 为固定大小( 512k ),可以被分割成两部分:头信息区域和数据区域。数据区域被分割成若干个 Run 。
- 在 Chunk 的数据区中一个或者多个页会被分成一个组,用于提供特定大小的内存分配,被称为" Run "(相同大小内存所在的页组),相同大小 Region 对应的 Run 属于同一个 Bin ( bucket )进行管理(如下图所示)。
- " Run "中的内存会被切分为固定小的内存块" Region ",作为最小的内存分配单元,作为实际内存地址返回给内存分配申请。
- 小对象( Small Object ) 根据 Small Object 对象大小,从 Run 中的指定 " region " 返回。
- 大对象( Large Object ) Large Object 大小比 chunk 小,比 page 大,会单独返回一个 " Run "(1或多个 page 组成)。
- 超大对象( Huge Object ) > 4 MB huge object 的内存直接采用 mmap 从 system memory 中申请(独立" Chunk "),并由一棵与 arena 独立的红黑树进行管理。
* Summary
- jemalloc 的核心是一个基于桶分配器( bin )。每个 arena 都有一组逻辑 bin ,每个 bin 都服务于特定的尺寸等级。分配是从具有足够大以适应分配请求的最小尺寸的 bin 进行的,它们之间的步长很小,可以减少碎片。
- 每个 arena 都有自己的锁,所以不同 arena 上的操作不会争抢锁。
- 临界区很短,仅当从 arena 中分配内存(共享 arena 线程间内部锁),或者分配 runs 的时候才需要保持锁定。
- 线程特定的内存缓存进一步减小数据竞争。
3. scudo ( Android 11 ~)
* Key Points ( salient points )
struct AndroidConfig { using SizeClassMap = AndroidSizeClassMap; #if SCUDO_CAN_USE_PRIMARY64 // 256MB regions typedef SizeClassAllocator64<SizeClassMap, 28U, 1000, 1000, /*MaySupportMemoryTagging=*/true> Primary; #else // 256KB regions typedef SizeClassAllocator32<SizeClassMap, 18U, 1000, 1000> Primary; #endif // Cache blocks up to 2MB typedef MapAllocator<MapAllocatorCache<32U, 2UL << 20, 0, 1000>> Secondary; template <class A> using TSDRegistryT = TSDRegistrySharedT<A, 2U>; // Shared, max 2 TSDs. };
* Primary Allocator
struct UnpackedHeader { uptr ClassId : 8; u8 State : 2; u8 Origin : 2; uptr SizeOrUnusedBytes : 20; uptr Offset : 16; uptr Checksum : 16; };
struct AndroidSizeClassConfig { static const uptr NumBits = 7; static const uptr MinSizeLog = 4; static const uptr MidSizeLog = 6; static const uptr MaxSizeLog = 16; static const u32 MaxNumCachedHint = 14; static const uptr MaxBytesCachedLog = 13; static constexpr u32 Classes[] = { 0x00020, 0x00030, 0x00040, 0x00050, 0x00060, 0x00070, 0x00090, 0x000b0, 0x000c0, 0x000e0, 0x00120, 0x00160, 0x001c0, 0x00250, 0x00320, 0x00450, 0x00670, 0x00830, 0x00a10, 0x00c30, 0x01010, 0x01210, 0x01bd0, 0x02210, 0x02d90, 0x03790, 0x04010, 0x04810, 0x05a10, 0x07310, 0x08210, 0x10010, }; static const uptr SizeDelta = 16; };
* Secondary Allocator
- 在 LargeBlock 的前后都增加了保护页以检测 heap-overflow 错误;
- LargeBlock Header 域主要记录了 LargeBlock 的内存布局信息以便于数据存取和管理;
- 剩余内存结构和 small block 布局一致,安全性考量和设计也一致(统一性)。
* Scudo内存分配和回收
NOINLINE void *allocate(uptr Size, Chunk::Origin Origin, uptr Alignment = MinAlignment, bool ZeroContents = false) { initThreadMaybe(); // 初始化TSD Array 和 Primary SizeClass地址空间 // skip trivials....... // If the requested size happens to be 0 (more common than you might think), // allocate MinAlignment bytes on top of the header. Then add the extra // bytes required to fulfill the alignment requirements: we allocate enough // to be sure that there will be an address in the block that will satisfy // the alignment. const uptr NeededSize = roundUpTo(Size, MinAlignment) + ((Alignment > MinAlignment) ? Alignment : Chunk::getHeaderSize()); // skip trivials....... void *Block = nullptr; uptr ClassId = 0; uptr SecondaryBlockEnd; if (LIKELY(PrimaryT::canAllocate(NeededSize))) { // 从 Primary Allocator分配small object ClassId = SizeClassMap::getClassIdBySize(NeededSize); DCHECK_NE(ClassId, 0U); bool UnlockRequired; auto *TSD = TSDRegistry.getTSDAndLock(&UnlockRequired); Block = TSD->Cache.allocate(ClassId); // If the allocation failed, the most likely reason with a 32-bit primary // is the region being full. In that event, retry in each successively // larger class until it fits. If it fails to fit in the largest class, // fallback to the Secondary. if (UNLIKELY(!Block)) { while (ClassId < SizeClassMap::LargestClassId) { Block = TSD->Cache.allocate(++ClassId); if (LIKELY(Block)) { break; } } if (UNLIKELY(!Block)) { ClassId = 0; } } if (UnlockRequired) TSD->unlock(); } // 如果分配的是大内存,或者Primary 无法分配小内存, // 则直接在Secondary Allocator进行分配 if (UNLIKELY(ClassId == 0)) Block = Secondary.allocate(NeededSize, Alignment, &SecondaryBlockEnd, ZeroContents); // skip trivials....... const uptr BlockUptr = reinterpret_cast<uptr>(Block); const uptr UnalignedUserPtr = BlockUptr + Chunk::getHeaderSize(); const uptr UserPtr = roundUpTo(UnalignedUserPtr, Alignment); void *Ptr = reinterpret_cast<void *>(UserPtr); void *TaggedPtr = Ptr; // skip trivials....... // 根据返回内存地址,设置chunk-header对象数据 Chunk::UnpackedHeader Header = {}; if (UNLIKELY(UnalignedUserPtr != UserPtr)) { const uptr Offset = UserPtr - UnalignedUserPtr; DCHECK_GE(Offset, 2 * sizeof(u32)); // The BlockMarker has no security purpose, but is specifically meant for // the chunk iteration function that can be used in debugging situations. // It is the only situation where we have to locate the start of a chunk // based on its block address. reinterpret_cast<u32 *>(Block)[0] = BlockMarker; reinterpret_cast<u32 *>(Block)[1] = static_cast<u32>(Offset); Header.Offset = (Offset >> MinAlignmentLog) & Chunk::OffsetMask; } Header.ClassId = ClassId & Chunk::ClassIdMask; Header.State = Chunk::State::Allocated; Header.Origin = Origin & Chunk::OriginMask; Header.SizeOrUnusedBytes = (ClassId ? Size : SecondaryBlockEnd - (UserPtr + Size)) & Chunk::SizeOrUnusedBytesMask; // 设置chunk-header,CheckSum用于完整性校验 Chunk::storeHeader(Cookie, Ptr, &Header); // skip trivials....... return TaggedPtr; }
- 当有线程通过 malloc 请求内存分配时,会通过符号重定向调用 scudo::allocator 的 allocate 函数。
- 进入 allocate 函数后,首先调用初始化函数,以完成 TSD 和 Primary 虚拟地址空间初始化。
- 根据 malloc 内存 size 判定是否可以通过 Primary Allocator 进行分配。
- 如果 Primary Allocator 分配的内存符合要求,计算 malloc size 对应的 SizeClass ,当前线程采用 TSD 的 Allocator::Cache 分配内存, Allocator::Cache 为每个 SizeClass 维护了一个 TransferBatch 链表,其中 TransferBatch 中是指向实际 Block 区域的指针。
- 如果 Allocator::Cache 无法分配内存,那么请求 Allocator 从对应 SizeClass 的 FreeList 中获取内存并 refill 到 Allocator::Cache 中。
- 如果 FreeList 中没有可用的内存, Allocator 需要从对应 SizeClass 的 class region 扩充空闲区域(调整MappedUser),并将内存区域切分为固定的 Block 大小,将可用的 Block 内存组织成 TransferBatch 添加到 FreeList ,并进一步 refill 到 Allocator::Cache 中供分配使用。
- 当我们分配小内存时,首先会检查最合适区域中是否有空闲位置,如果没有,则会去高一级区域中分配。例如在32 Bytes SizeClass Region 无法内存出内存,那么会逐步尝试从48 Bytes ,64 Bytes SizeClass Region 中进行分配(小内存区域耗尽)。
- 如果内存尚未分配成功,则采用 Secondary Allocator 继续尝试分配内存。
- 如果获取了有效的内存地址,则根据返回内存地址,计算 CheckSum 并设置 chunk-header 对象数据。
- 将返回内存地址通过 malloc 返回。
* Summary
- 内存
- Primary Allocator 会在独立划分的一块虚拟地址空间( RegionSize* ClassNums ~ 2G )中采用随机化策略分配,为了安全性的同时引入了内存碎片。
- Primary 分配的内存中, chunk header 记录用于内存检查的信息,有效数据比例较低(特别是小内存对象,32 bytes 有效数据为 50% )。
- Secondary 分配的内存区前后都有保护页,内存空间使用率较低。
-
- 性能
- 运行期需要进行安全性检测( heap-overflow etc .),完整性检测( CheckSum )等安全策略,存在性能损失。
-
4. PartitionAlloc
- 统一跨平台的内存分配,增强安全性。
- 在不影响安全性和性能的前提下,降低碎片,减少内存占用。
- 定制分配器以优化 Chrome 的性能。
* Key Points ( salient points )
* Central Partition Allocator [16]
* Partition Layer
- Per-thread cache
为了缓解多线程内存分配的数据竞争问题,每个线程的数据缓存区( TLS )保存了少量用于常用中小内存分配的 Slots 。因为这些 Slots 是按线程存储的,所以可以在没有锁的情况下分配它们,并且只需要更快的线程本地存储查找,从而提高进程中的缓存局部性。每个线程的缓存已经过定制以满足大多数请求,通过批量分配和释放第二层的内存,分期锁获取,并在不捕获过多内存的情况下进一步改善局部性。
- Slot span free-list
- Slot span management
- Buffer 分区:主要分配变长对象或者是内容可能会被用户脚本篡改的对象,如 Vector 、 HashTable 、 ArrayBufferContent 、 String 等容器类对象;
- Node分区(之前版本叫 model object 分区):主要分配 dom 节点对象,通过重写 Noded 的 new / delete 运算符实现;
- LayoutObject 分区:主要分配 layou 相关对象,如 LayoutObject 、 PaintLayer 、双向字体 BidiCharacterRun 等对象;
- FastMalloc 分区:主要分配除其他三种类型之外的对象(通用对象分配器),大量功能逻辑的内部对象都归于此分区;
* 安全性
- 线性溢出不会破坏分区 - guard page。
- 元数据记录在专用区域(不是每个对象旁边)。线性上溢或下溢不会破坏元数据。
- 大内存分配在开始和结束时被保护分页 - guard page。
- 桶有助于在不同地址上分配不同大小的对象。一页只能包含类似大小的对象。
* Summary
- 调用者可以根据需要创建任意数量的分区。分区的直接内存成本是最小的,但碎片导致的隐含成本不可低估。
- PartitionAlloc 保证不同的分区存在于进程地址空间的不同区域。当调用者释放了一个分区中一个页面中包含的所有对象时, PartitionAlloc 将物理内存返回给操作系统,但继续保留地址空间区域。PartitionAlloc 只会为同一个分区重用一个地址空间区域。但在一定程度上,它会浪费虚拟空间( virtual address space )。
5. Overview
内存分配器 | 说明 |
---|---|
dlmalloc | https://github.com/ARMmbed/dlmalloc· 非线程安全,多线程分配效率低· 内存使用率较低(内存碎片多)· 分配效率低· 安全性低 |
jemlloc | https://github.com/jemalloc/jemalloc/blob/dev/INSTALL.md· 代码体积较大,元数据占据内存大· 内存分配效率高(基于run(桶) + region)· 内存使用率高· 安全性中 |
scudo | https://android.googlesource.com/platform/external/scudo/· 更注重安全性,内存使用效率和性能上存在一定的损失· 内存分配效率偏高(sizeclass + region, 安全性+checksum校验)· 内存使用率低(元数据,安全区,地址随机化内存占用多)· 安全性高( 安全性+checksum校验,地址随机化,保护区) |
partition-alloc | https://source.chromium.org/chromium/chromium/src/+/main:base/allocator/partition_allocator/PartitionAlloc.md· 代码体积小· 内存使用率偏高(相比于jemalloc多保护区,基于range的分配)· 安全性偏高(相比sudo少安全性,完整性校验,地址随机化)· 内存分配效率高(基于bucket + slot,分区特定优化)· 支持"全"平台 PartitionAlloc Everywhere (BlinkOn 14) :https://www.youtube.com/watch?v=QfY-WMFjjKA |
Allocator | QPS (higher is better) | Max RSS (lower is better) |
---|---|---|
tcmalloc (internal) | 410K | 357MB |
jemalloc | 356K | 1359MB |
dlmalloc (glibc) | 295K | 333MB |
mesh | 142K | 710MB |
portableumem | 24K | 393MB |
hardened_malloc* | 18K | 458MB |
guarder | FATALERROR** | |
freeguard | SIGSEGV*** | |
scudo (standalone) | 400K | 318MB |
6. Rererence

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Windows Terminal Preview 1.16 发布
Windows Terminal Preview 1.16 现已发布,具体更新内容如下: Theming 通过引入 themes,为用户增添了更多可自定义的可能 themes是一个全局属性,可以包含各种 themes 对象,这些对象将出现在设置用户界面的外观页面的主题下拉菜单中。下面是每个 themes 对象可以接受的对象。注意:主题只能使用 JSON 文件进行编辑,但它们会出现在设置 UI 的主题下拉菜单中。 Sample JSON "themes": [ { "name": "Grace Kelly", "tab": { "background": "#00515EFF", "showCloseButton": "always", "unfocusedBackground": null }, "tabRow": { "background": "#061612FF", "unfocusedBackground": "#061612FF" }, "window": { "applicationTheme": "dark" } } ]...
- 下一篇
PhotoDemon —— 免安装轻量级图像编辑器
PhotoDemon 是一个超轻量级、免安装的开源图像编辑器。PhotoDemon 体积仅有14 MB ,但提供了全面的图像编辑工具。它可以作为便携式应用程序在任何 Windows 机器(XP 到 Win 11)上运行,可以放在 U 盘或 SD 卡上随身携带,因为它不需要安装,也不需要管理员权限即可运行。 集成宏录制和批处理 可以将复杂的编辑动作记录为宏(类似于 Office 软件)。内置批处理器可让您将宏应用到整个图像文件夹。 可用性 许多开源照片编辑器可用性的噩梦。PhotoDemon 尽量避免可用性问题,比如支持实时效果预览、保存/加载所有工具上的预设、无限制的撤消/重做、“淡入淡出最后一个动作”、键盘加速器、鼠标滚轮和 X 按钮支持以及描述性图标等小触摸,使其快速且易于使用。 专业级功能和工具 广泛的文件格式支持,包括 Adobe Photoshop (PSD)、Corel PaintShop Pro (PSP)、GIMP (XCF) 和所有主要的相机 RAW 格式 高级多层支持,包括可编辑的文本图层和非破坏性图层修改 色彩管理工作流程,包括对嵌入式 ICC 配置文件的全面...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS7设置SWAP分区,小内存服务器的救世主
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果