1、简述
在 Java 中,对象的创建虽然看起来只是一个简单的 new
操作,实际上背后涉及了复杂的内存分配策略。本文将深入介绍 JVM 内存管理中的两个重要概念:指针碰撞(Pointer Bump) 和 空闲列表(Free List),并结合实例讲解其应用与性能差异。
2、对象分配的背景
在 Java 虚拟机中,当程序通过 new
创建一个对象时,JVM 会从堆(Heap)中分配内存。JVM 的内存分配策略主要取决于堆内存的组织方式。
常见的堆结构:
🔹 连续堆空间(如新生代 Eden 区)
🔹 非连续堆空间(如老年代或存在内存碎片时)
基于这些结构,就产生了两种不同的内存分配方式:指针碰撞 和 空闲列表。
3、指针碰撞(Pointer Bump)
原理说明
当堆是连续空间时,比如新生代的 Eden 区,JVM 可以用一个“指针”指向堆的下一个可用内存地址。
每次分配新对象时,只需将该指针向前“碰撞”指定的字节数,完成分配。这种方式就叫做指针碰撞(Bump-the-pointer)。
内存布局示意图
| Used Memory | Free Space |
↑
Allocation Pointer
当创建新对象时:
allocation_pointer += object_size;
特点
🔹 分配速度极快,几乎是线性增长。
🔹 不需要记录空闲块链表,适用于内存碎片较少的场景。
🔹常用于新生代(Eden)对象分配。
4、空闲列表(Free List)
原理说明
当堆是非连续空间或存在大量内存碎片时(如老年代),JVM 会维护一个空闲块列表(Free List)。每次分配对象时,从列表中查找符合大小要求的内存块进行分配。
内存布局示意图
Free List: [Block A (32B)] → [Block B (64B)] → [Block C (128B)] ...
分配时从链表中找到最合适的块(First Fit / Best Fit),并进行分配。
特点
🔹 内存使用更灵活,适合回收后的堆碎片。
🔹 分配速度慢于指针碰撞。
🔹 常用于老年代(Tenured)对象分配。
5、JVM 如何选择分配方式?
JVM 中的对象分配方式由以下条件决定:
🔹 是否启用内存压缩(UseCompressedOops)
🔹堆是否连续
🔹 GC 类型(如 G1、CMS)
🔹 是否开启线程本地分配缓冲区(TLAB)
在新生代中,特别是 Eden 区,JVM 多采用指针碰撞方式。而在老年代或非压缩内存布局下,则更多使用空闲列表。
6、实践样例(日志观测)
虽然我们不能直接操控 JVM 的分配方式,但可以借助参数与 GC 日志分析其行为。
示例代码
public class AllocationTest {
public static void main(String[] args) {
for (int i = 0; i < 10_000; i++) {
byte[] data = new byte[1024]; // 每次分配 1KB
}
}
}
JVM 启动参数:
java -XX:+UseSerialGC -Xms32m -Xmx32m -XX:+PrintGCDetails AllocationTest
查看 GC 日志,可以观察 Eden 区是否频繁分配、是否触发 Minor GC:
[GC (Allocation Failure) [DefNew: 8192K→1024K(9216K), 0.0034286 secs] ...
表示 Eden 区采用指针碰撞分配,但内存不足时触发了 GC。
TLAB:线程本地分配缓冲区(加速指针碰撞)
为了加快多线程环境下的对象分配,JVM 引入了 TLAB。每个线程分配一块私有内存区域,用于快速进行指针碰撞。
可通过以下参数观察其启用状态:
-XX:+UseTLAB -XX:+PrintTLAB
7、总结
维度 | 指针碰撞(Pointer Bump) | 空闲列表(Free List) |
---|---|---|
适用内存结构 | 连续堆(如新生代) | 非连续堆(如老年代) |
分配速度 | 极快 | 较慢(遍历空闲块) |
是否支持碎片 | ❌ 不支持 | ✅ 支持 |
线程安全性 | 需配合 TLAB 或加锁 | 需加锁 |
理解指针碰撞与空闲列表,有助于你:
🔹理解 JVM 内存分配背后的策略与优化
🔹分析 GC 日志中的行为与瓶颈
🔹 编写性能友好的代码,尽可能让对象在新生代短命
在实际项目中,建议结合 jstat
、VisualVM
、GC Viewer
等工具,深入分析对象分配与内存回收过程。