深入剖析 Chrome PartitionAlloc 内存池源码原理与性能调优实践
一、前言
在现代浏览器中,内存分配的性能和稳定性直接影响整体用户体验。Chromium/Chrome 作为世界上最复杂的 C++ 应用之一,为了高效、安全地管理内存,自研了一套高性能的内存池机制——PartitionAlloc。
PartitionAlloc 在 Chrome 中几乎无处不在:DOM 节点的分配、V8 堆外内存、IPC 消息缓冲、UI 渲染对象,都可能通过它来完成。与传统 malloc
/ free
相比,它更强调:
内存安全:结合 BackupRefPtr (BRP)、Pointer Scanning (PCScan) 等机制,降低 UAF 风险。
性能优化:引入线程缓存(ThreadCache)、批量分配(FillBucket)、快速路径分配,减少锁争用和系统调用。
碎片控制:通过 SlotSpan / Bucket / SuperPage 的层次化管理,降低内存浪费。
跨平台一致性:统一抽象在 Windows、Linux、macOS 上的虚拟内存分配。
这篇文章将从源码视角深入解析 Chrome PartitionAlloc 内存池的机制,并结合实际工程调优经验,给出一份“从架构到实战”的完整指南。
二、PartitionAlloc 内存池架构解析
PartitionAlloc 的整体设计分为三层:
SuperPage (2MB)
内存分配的基本单位,每次至少申请 2MB。
SuperPage 的首尾部分保留作为保护页(guard page)。
在中间区域,划分多个 SlotSpan。
SlotSpan (由多个系统页组成)
每个 SlotSpan 管理一组等大小的对象(slot)。
SlotSpan 的元数据(metadata)记录 freelist、slot 数量等信息。
Bucket (桶)
每个 bucket 管理相同大小的 slot。
典型分布:8B、16B、32B…到 256KB。
大于 256KB 的分配采用 DirectMap,直接映射单独的大块内存。
关键常量定义
constexpr size_t PartitionPageSize = 16 * 1024; // 16KB constexpr size_t kSuperPageSize = 2 * 1024 * 1024; // 2MB constexpr size_t SystemPageSize = 4 * 1024; // 4KB
PartitionPageSize (16KB):内部元数据和 SlotSpan 的最小粒度。
kSuperPageSize (2MB):每次分配的大块内存。
SystemPageSize (4KB):操作系统页面大小。
三、源码关键类说明
PartitionAlloc 的核心代码位于 base/allocator/partition_allocator/
,几个重要类:
1. PartitionRoot
整个内存池的入口类。
负责初始化 bucket、管理 SuperPage。
接口:
AllocFromBucket()
:分配对象。Free()
:释放对象。
2. PartitionBucket
管理一组固定大小的对象 slot。
内部维护活跃 SlotSpan 链表:
active_slot_spans_head
empty_slot_spans_head
decommitted_slot_spans_head
快速路径:直接从 freelist 取内存。
慢速路径:触发新建 SlotSpan 或 SuperPage。
3. SlotSpanMetadata
管理 SlotSpan 的元信息。
字段:
freelist_head
:指向空闲 slot。num_allocated_slots
:当前分配数量。num_unprovisioned_slots
:剩余可分配 slot 数。
4. ThreadCache
线程本地缓存,减少锁争用。
接口:
GetFromCache()
:快速分配。MaybePutInCache()
:回收时加入缓存。RunPeriodicPurge()
:周期清理(默认 2s)。
5. PartitionDirectMap
处理大于 256KB 的分配,直接
mmap
/VirtualAlloc
。分配粒度对齐到系统页。
四、SuperPage / SlotSpan / FreeList 图示
下面给出一个结构示意图,展示 SuperPage → SlotSpan → Slot → FreeList 的关系:
+-----------------------------------------------------------+ | SuperPage (2MB) | | | | [Guard Page 16KB] | | +-------------------+ | | | Metadata Area | <-- SlotSpanMetadata | | +-------------------+ | | | SlotSpan #1 | [slots of size 64B] | | | +---------+ | | | | | slot_1 | --> freelist_head → slot_3 → slot_7 ... | | | +---------+ | | | | slot_2 | | | | +---------+ | | +-------------------+ | | | SlotSpan #2 | [slots of size 128B] | | +-------------------+ | | | | [Guard Page 16KB] | +-----------------------------------------------------------+
freelist_head 存储在 SlotSpanMetadata 内部。
每个空闲 slot 的起始字节存储下一个空闲 slot 的地址(单链表)。
内存布局高度紧凑,减少碎片。
五、内存分配流程
Chrome 的分配流程可以分为三步:
调整大小
向上对齐到对应 bucket 的 slot 大小。
预留空间存放 cookie、guard byte。
分配
线程缓存:
ThreadCache::GetFromCache()
。分配器快速路径:
PartitionRoot::AllocFromBucket()
,直接 freelist 弹出。慢速路径:
PartitionBucket::SlowPathAlloc()
。
初始化
如有需要,零初始化。
调试模式下写入 cookie。
源码片段:
void* PartitionRoot::Alloc(size_t size) { PartitionBucket* bucket = SizeToBucket(size); SlotSpanMetadata* slot_span = bucket->active_slot_spans_head; if (slot_span && slot_span->freelist_head) { return slot_span->freelist_head->Pop(); } return SlowPathAlloc(bucket, size); }
六、内存释放机制
释放同样分为 线程缓存 → 普通释放 → 慢速路径 → 系统回收:
线程缓存:
ThreadCache::MaybePutInCache()
。如果缓存已满,批量返还给 PartitionRoot。
普通释放:
插入 SlotSpan 的 freelist 头部。
如果 Span 曾经标记为满,则重新挂回活跃列表。
慢速路径:
如果 Span 全部释放,迁入 empty_list。
可能触发
Decommit
,释放物理内存。对大块内存(DirectMap)直接
munmap
/VirtualFree
。
定期释放:
MemoryReclaimer::Reclaim()
,默认 4s 周期运行。释放未使用的系统页,降低 RSS。
七、性能调优方向
在工程落地时,可以从以下几个方面优化:
线程缓存大小
过大:浪费内存。
过小:频繁 fallback,增加锁开销。
调整策略:通过
ThreadCache::SetThreadCacheSizeLimits()
调优。
延迟回收策略
默认 4s 的
MemoryReclaimer
可以缩短或延长。高频分配/释放场景适合更快周期。
大对象分配 DirectMap
对于 >256KB 的 DirectMap,避免过度分配。
可通过分配对齐策略减少内部碎片。
BRP 与 PCScan 的选择
BRP (BackupRefPtr):运行时检测 UAF,增加内存开销。
PCScan (Pointer Scanning):周期扫描指针,适合大内存场景。
可在 GN args 中按需启用:
use_backup_ref_ptr = true enable_pointer_compression = true enable_pcscan = false
统计与监控
启用 tracing,观察
MemoryInfra.PartitionAlloc
。调用 allocator hooks,收集实时分配情况。
八、实战步骤
1. 本地构建带 BRP 的 Chromium
gn gen out/brp --args="is_debug=false use_backup_ref_ptr=true" ninja -C out/brp chrome
2. 启用 PCScan
gn gen out/pcscan --args="enable_pcscan=true"
3. 收集运行时统计
方式 1:Tracing
打开about://tracing
,勾选MemoryInfra
→PartitionAlloc
。方式 2:Histogram
在代码中添加:UMA_HISTOGRAM_COUNTS_1000("PartitionAlloc.AllocSize", size);
方式 3:Allocator hooks
base::allocator::SetHooks(&my_hooks);
九、工程落地建议
如果你希望将 PartitionAlloc 引入到非浏览器项目(如嵌入式/服务端组件):
构建系统
GN: 保留
partition_alloc/partition_alloc.gni
。CMake: 需要手动导入
.cc/.h
文件,并定义PA_BUILDFLAG(...)
。
启用/禁用建议
禁用 allocator_shim:避免和系统 malloc 冲突。
禁用 sanitizers:ASan 下 PartitionAlloc 与系统 malloc 共存会冲突。
PGO/LTO:需要在 GN args 中设置
use_partition_alloc=true
。
兼容性
Windows: 使用
VirtualAlloc
。Linux: 使用
mmap
+madvise
。macOS: 使用
mmap
+ VM tag。
十、常见问题 FAQ
Q1: 为什么分配了 1KB 内存,实际 RSS 增加了 16KB?
因为最小粒度是 PartitionPage (16KB),小分配被批量管理。
Q2: 为什么大于 256KB 的对象不走内存池?
设计选择,避免巨型对象污染小对象池。
Q3: 如何判断某个对象是 DirectMap 分配?
通过
PartitionAllocGetDirectMapMetadata(ptr)
。
Q4: ThreadCache 会导致内存泄漏吗?
不会。ThreadCache 有周期清理机制,且在线程退出时自动回收。
Q5: 如何在生产环境观察内存池状态?
开启
--enable-memory-infra
,结合 Chrome 内置 DevTools → Performance → Memory。
十一、总结
通过 PartitionAlloc,Chromium 实现了一套跨平台、高性能、安全的内存池机制。
架构层面:SuperPage → SlotSpan → Bucket → FreeList,层次分明,碎片可控。
源码实现:
PartitionRoot
负责分配,ThreadCache
提升性能,MemoryReclaimer
负责回收。调优方向:可通过缓存大小、回收周期、DirectMap 策略优化性能。
工程落地:不仅适合浏览器,也可以用于服务端、嵌入式系统,提供稳定高效的内存分配。
未来,随着 Chrome 在 隐私沙盒、性能优化、内存安全 方面的持续投入,PartitionAlloc 也会不断演进,例如更智能的垃圾回收、更轻量的指针保护。
对于开发者而言,理解 PartitionAlloc 的原理和源码实现,不仅能帮助我们优化浏览器,还能将其经验迁移到其他高性能项目中。