在 JVM 中,对象内存分配主要有两种策略:指针碰撞(Bump-the-Pointer) 和 空闲列表(Free List)。它们的核心区别在于 内存管理的粒度和适用场景。
1. 指针碰撞(Bump-the-Pointer)
(1)核心思想
- 连续分配:在 连续的内存空间(如 TLAB 或 Eden 区)中,通过移动指针快速分配对象。
- 无碎片(初始状态):适用于完全未分配或已整理的内存区域。
(2)分配过程
- 维护一个 当前空闲指针(
current_ptr),指向下一个可用内存地址。 - 分配对象时,直接移动指针:
void* allocate(size_t size) { void* obj = current_ptr; current_ptr += size; // 指针后移 return obj; } - 检查剩余空间:
current_ptr + size ≤ end_ptr。
(3)特点
| 优点 | 缺点 |
|---|---|
| ✅ 极快(仅指针加减) | ❌ 仅适用于连续内存 |
| ✅ 无锁(TLAB 内线程私有) | ❌ 需预分配固定大小空间 |
| ✅ 内存紧凑(减少碎片) | ❌ 空间不足时需触发 GC 或扩容 |
(4)适用场景
- 新生代 Eden 区(默认分配方式)。
- TLAB(Thread-Local Allocation Buffer)。
- 栈上分配(逃逸分析优化后)。
2. 空闲列表(Free List)
(1)核心思想
- 离散分配:维护一个 空闲内存块的链表,分配时从链表中查找合适大小的块。
- 容忍碎片:适用于存在内存碎片或非连续内存的场景(如老年代)。
(2)分配过程
- 维护一个 空闲内存块的链表(按大小或地址排序)。
- 分配对象时:
- 遍历链表,找到第一个 ≥ 对象大小的块(First-Fit 或 Best-Fit)。
- 分割块(剩余部分放回链表)或直接占用整个块。
- 回收对象时,将内存块重新插入链表(可能合并相邻空闲块)。
(3)特点
| 优点 | 缺点 |
|---|---|
| ✅ 灵活(支持碎片化内存) | ❌ 分配速度较慢(需遍历链表) |
| ✅ 适合大对象或老年代 | ❌ 可能产生内存碎片 |
| ✅ 无需连续内存 | ❌ 需同步锁(多线程竞争) |
(4)适用场景
- 老年代(Mark-Compact 或 CMS 回收后)。
- 直接分配大对象(绕过 TLAB)。
- 自定义内存池(如 Netty 的 PooledByteBuf)。
3. 对比总结
| 特性 | 指针碰撞 | 空闲列表 |
|---|---|---|
| 分配速度 | 极快(O(1)) | 较慢(O(n) 链表遍历) |
| 内存连续性 | 连续 | 可能碎片化 |
| 同步开销 | 无锁(TLAB 内) | 需加锁/CAS |
| 适用区域 | 新生代 Eden/TLAB | 老年代、大对象 |
| 碎片问题 | 无(初始状态) | 可能产生碎片 |
| GC 影响 | Young GC 后重置指针 | Full GC 后重建空闲列表 |
4. JVM 中的实际应用
(1)新生代:指针碰撞为主
- Eden 区:默认使用指针碰撞(通过 TLAB 优化)。
- TLAB 分配失败时:退化为全局 Eden 区的指针碰撞(需 CAS 竞争)。
(2)老年代:空闲列表为主
- CMS 回收后:使用空闲列表管理内存块。
- G1 的 Region:每个 Region 内部可能混合使用两种策略。
(3)特殊场景
- 大对象直接进入老年代:通过
-XX:PretenureSizeThreshold控制,绕过 TLAB 和指针碰撞。 - 堆外内存(DirectByteBuffer):通常由操作系统的
malloc(空闲列表变种)管理。
5. 优化建议
- 优先利用 TLAB:
- 减少全局指针碰撞的竞争(默认开启
-XX:+UseTLAB)。
- 减少全局指针碰撞的竞争(默认开启
- 避免过早晋升老年代:
- 老年代的空闲列表分配较慢,可通过
-XX:MaxTenuringThreshold调整。
- 老年代的空闲列表分配较慢,可通过
- 监控碎片化:
- 老年代碎片过多时,考虑使用
-XX:+UseCMSCompactAtFullCollection(CMS 压缩)。
- 老年代碎片过多时,考虑使用
6. 总结
- 指针碰撞:追求速度,适合连续内存(如新生代)。
- 空闲列表:追求灵活性,适合碎片化内存(如老年代)。
- JVM 的选择:根据内存区域和对象特性动态切换策略,平衡性能和内存利用率。
类比:
- 指针碰撞像 自助餐排队,按顺序快速取餐(但必须排队)。
- 空闲列表像 点餐系统,自由选择空闲桌子(但需要查找和管理)。