摘要:Spark 内存管理是性能调优的核心战场------理解 UnifiedMemoryManager 的 Execution/Storage 动态借用机制、Tungsten 页式分配、Off-Heap 与 On-Heap 的权衡、以及 acquire → evict → spill 的分配溢出流程,是避免 OOM、提升 Shuffle 效率的关键。本文从 JVM Heap 分区架构、Unified vs Static 对比、内存分配失败处理链、Tungsten MemoryAllocator、堆外内存与常见 OOM 调优六个维度,配合 2 张原创架构图 + 源码追踪,彻底拆解 Spark 内存管理全貌。
关键词:UnifiedMemoryManager, Execution Memory, Storage Memory, Tungsten, Off-Heap, Eviction, Spill, OOM 调优
一、开篇:你的 Executor 内存花在哪了?
一个 spark.executor.memory=4g 的 Executor,实际可用内存远小于 4GB。Spark 将 JVM Heap 划分为三层:
yaml
4GB Executor Heap 默认划分
├── Reserved Memory: 300MB (固定,不可调)
├── User Memory: 1.48GB (1 - 0.6) × (4G - 300M)
│ └── 存放 RDD 闭包引用、用户自定义数据结构
└── Spark Memory (统一池): 2.22GB 0.6 × (4G - 300M)
├── Storage Memory: 1.11GB (默认 50%)
└── Execution Memory: 1.11GB (默认 50%)
两者可动态互借!
二、UnifiedMemoryManager 架构全景

2.1 三层 Heap 架构
scala
// UnifiedMemoryManager.getMaxExecutionMemory()
val maxMemory = systemMemory - reservedMemory // 可用总内存
val sparkMemory = maxMemory * conf.get(MEMORY_FRACTION) // Spark 池
val storageMemory = sparkMemory * conf.get(STORAGE_FRACTION) // Storage 初始占比
val executionMemory = sparkMemory - storageMemory // Execution 初始占比
// 动态借用: Execution 可借用 Storage 空闲部分
// Storage 也可借用 Execution 空闲部分(但 Execution 需要时可踢回)
2.2 动态借用规则
yaml
Execution 借用 Storage:
Storage 有闲置 → Execution 直接拿走 → 不归还
Storage 借用 Execution:
Execution 有闲置 → Storage 暂时使用
→ Execution 需要时会触发 Eviction → 淘汰 Storage Block
核心: Execution 优先级高于 Storage
acquireExecutionMemory() → 先用自己的 pool → 不够→借Storage→还不够→spill磁盘
三、内存分配与溢出流程

3.1 三步处理
scss
MemoryConsumer.acquireMemory(requiredSize):
① 检查 Execution Pool 余额 → 够用直接分配
② 余额不足 → evictBlocksToFreeSpace(required)
- 从 Storage Pool 淘汰 RDD Block 缓存
③ 仍不够 → MemoryConsumer.spill() 自身溢写磁盘
- 释放内存后重试 acquire
3.2 核心源码
scala
// UnifiedMemoryManager.acquireExecutionMemory()
override def acquireExecutionMemory(numBytes: Long, ...): Long = {
synchronized {
// 尝试从 Storage 借: 淘汰 Storage Block 释放空间
val memoryReclaimableFromStorage = math.max(
storagePool.memoryFree,
storagePool.poolSize - storageRegionSize)
val spaceReclaimed = storagePool.evictBlocksToFreeSpace(...)
// 更新可用总量
val maxToGrant = math.min(numBytes,
math.max(0, maxExecutionMemory - executionPool.poolSize))
executionPool.incrementPoolSize(maxToGrant)
maxToGrant
}
}
四、Tungsten 页式内存管理
scala
// Tungsten 内存模型
MemoryAllocator (Heap / Unsafe)
→ allocate(size) → MemoryBlock(obj/offset, length)
TaskMemoryManager
→ allocatePage(size) → 64-bit 逻辑地址
→ pageTable 逻辑地址到物理地址映射
→ 高 13bit: pageNumber (最多 8192 页)
→ 低 51bit: offsetInPage
优势:
├── 不受 GC 影响(Unsafe 模式)
├── 精确控制内存生命周期
├── 64 位单一地址空间
└── 页式管理减少碎片
properties
spark.memory.offHeap.enabled=true # 开启堆外内存
spark.memory.offHeap.size=2g # 堆外大小
# 堆外内存独立于 JVM Heap,不受 GC 影响
五、Unified vs Static --- 为什么必须 Unified?
makefile
StaticMemoryManager (Legacy) 问题:
Execution 池 + Storage 池 = 固定分割,互不借用
→ Execution 满而 Storage 空 → 直接 OOM
UnifiedMemoryManager:
统一池 + 动态借用 → 利用率最大化
Spark 1.6+ 默认 → 生产环境唯一推荐
六、OOM 场景与调优
常见 OOM
yaml
① User Memory OOM: 闭包引用了大对象 → 广播变量解决
② Execution Memory OOM: Shuffle 数据量过大 → 增大 fraction
③ Storage Memory OOM: 缓存过多 → 减少缓存或用 DISK_ONLY
④ Off-Heap OOM: spark.memory.offHeap.size 不足 → 增大
调优公式
scss
Execution 可用内存 (最小保证)
= (Heap - 300MB) × spark.memory.fraction × (1 - spark.memory.storageFraction)
调优策略:
① Shuffle 密集型: 增大 fraction (0.6→0.75) 或 减少 storageFraction (0.5→0.3)
② Cache 密集型: 增大 storageFraction (0.5→0.7)
③ GC 压力大: 开启堆外内存
④ User Memory 不足: 增大 Heap 或 减少闭包变量大小
七、总结
-
三层 Heap:Reserved(300M) + User((1-f)×Heap) + Spark(f×(Heap-300M))。Execution/Storage 动态借用。
-
分配失败链:Execution Pool → 借 Storage(evict) → 自身 spill 磁盘。Execution 优先级 > Storage。
-
调优关键:fraction 控 Spark 池大小、storageFraction 控 Storage 占比、offHeap 解 GC 之困。
作者 :starzy
博客 :blog.starzy.cn
GitHub :starzy1990.github.io
专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践