Skip to content

对象分配过程(一般/特殊情况)

分配即分配,JVM 怎么做

对象分配是 Java 程序中最频繁的操作之一。JVM 需要在堆中为每个 new 对象找到一块空闲空间,并初始化它。

一般分配过程:TLAB + 指针碰撞

指针碰撞(Bump the Pointer)

当堆内存规整时(即已分配的对象和空闲区域严格分开,中间没有碎片),JVM 只需要维护一个指针,指向空闲区域的起始位置。分配时,把指针向后移动对象大小的距离即可:

┌─────────────────────────┬──────────────────┐
│      已分配区域          │     空闲区域      │
│  [Obj1][Obj2][Obj3]     │     Bump Pointer│
└─────────────────────────┴──────────────────┘

                         分配新对象时
                         指针向前移动

指针碰撞非常高效,但前提是堆必须规整——这由 GC 决定。

本地线程分配缓冲(TLAB)

这是 JVM 中最重要的分配优化之一。

多个线程同时在堆上分配对象时,如果都用指针碰撞,需要同步机制(CAS)来保证线程安全——这会严重降低并发分配的性能。

TLAB 的解决思路:每个线程在 Eden 区预先分配一块专属区域(Thread-Local Allocation Buffer),线程在自己的 TLAB 内分配对象,无需任何同步。

线程 A 的 TLAB         线程 B 的 TLAB         线程 C 的 TLAB
┌────────────┐        ┌────────────┐        ┌────────────┐
│ ObjA1     │        │ ObjB1     │        │ ObjC1     │
│ ObjA2     │        │ ObjB2     │        │ ObjC2     │
│ (继续分配) │        │ (继续分配) │        │ (继续分配) │
└────────────┘        └────────────┘        └────────────┘
      ↑ Eden 区剩余空间(非 TLAB 区域)                ↑
                                          更多线程的 TLAB
java
public class TLABDemo {
    public static void main(String[] args) {
        // 在 TLAB 中分配了大量小对象
        for (int i = 0; i < 1000000; i++) {
            // 每个线程在自己的 TLAB 中分配,互不干扰
            // 分配速度接近栈分配
            byte[] data = new byte[64];
        }
    }
}

完整分配流程

new Object()


检查:类是否已加载?
      │ 否 → 类加载


计算对象大小(在编译时确定)


检查:能否在 TLAB 中分配?

      ├── 能:TLAB 内指针碰撞分配

      └── 不能:尝试在 Eden 区分配

                  ├── 成功:指针碰撞分配
                  └── 失败:触发 Minor GC


                          回收后重试分配

                              ├── 成功:分配
                              └── 失败:对象太大 → 直接在老年代分配

                                          ├── 成功:分配
                                          └── 失败:Full GC → OOM

特殊情况一:大对象直接进老年代

超过阈值的对象不在年轻代分配,直接在老年代分配。

java
// 设置阈值为 1MB
// java -XX:PretenureSizeThreshold=1048576
public class LargeObject {
    public static void main(String[] args) {
        // 超过阈值的大数组直接在老年代分配
        // 避免频繁的大对象 Minor GC
        byte[] hugeData = new byte[2 * 1024 * 1024];  // 2MB
    }
}

大对象直接进老年代的原因:如果一个大对象在年轻代分配,Minor GC 后它肯定还会存活(因为太大无法进入 Survivor 区),直接晋升老年代。与其让它每次 Minor GC 都经历一次复制,不如直接放在老年代。

特殊情况二:长期存活对象晋升

对象每经历一次 Minor GC 且存活,年龄计数器就加 1。当年龄达到 MaxTenuringThreshold 时,对象晋升到老年代。

java
public class AgingToOld {
    static class CacheNode {
        byte[] data = new byte[10 * 1024];  // 模拟缓存节点
    }

    public static void main(String[] args) {
        // 这个缓存节点存活时间很长
        CacheNode cache = new CacheNode();
        // 使用 16 次(超过默认阈值 15)
        for (int i = 0; i < 16; i++) {
            // 第 16 次 Minor GC 后,cache 晋升老年代
            useCache(cache);
        }
    }
    static void useCache(CacheNode c) { /* 模拟使用缓存 */ }
}

特殊情况三:动态年龄判定

即使没达到 MaxTenuringThreshold,如果 Survivor 区内相同年龄的所有对象大小之和超过 Survivor 区的一半,年龄 >= 该年龄的所有对象直接晋升老年代。

java
public class DynamicPromotion {
    public static void main(String[] args) {
        // 场景:大量对象同时在 Survivor 区
        // 如果这批对象的年龄相同且总和超过 Survivor 一半
        // 即使年龄小于 MaxTenuringThreshold,也会晋升
        for (int i = 0; i < 1000; i++) {
            byte[] obj = new byte[100];  // 累积约 100KB
        }
        // 假设 Survivor 只有 200KB
        // 1000 个 100 字节对象 = 100KB,占 Survivor 50%
        // 下次 Minor GC 时,这批对象全部晋升老年代
    }
}

特殊情况四:空间分配担保

Minor GC 之前,JVM 检查老年代最大可用连续空间是否大于年轻代所有对象的总大小。

java
public class AllocationGuarantee {
    public static void main(String[] args) {
        // 假设年轻代所有对象总大小 = 900MB
        // 老年代最大可用连续空间 = 500MB
        // 500MB < 900MB → 担保失败,触发 Full GC(而非 Minor GC)
        // Full GC 会整理老年代,释放空间后再 Minor GC
    }
}

JDK 8 Update 24 之后,担保失败的逻辑被简化——无论是否担保失败,都会先尝试 Minor GC,如果失败再 Full GC。

对象分配总结流程图

new 关键字


┌─────────────────────────────┐
│  检查类是否已加载            │
│  否 → 先类加载              │
└─────────────┬───────────────┘

┌─────────────────────────────┐
│  计算对象所需空间             │
└─────────────┬───────────────┘

┌─────────────────────────────┐
│  TLAB 分配?                 │
├──── 是 ──→ TLAB 内碰撞分配  │
│  否                          │
│  ▼                           │
│  Eden 区指针碰撞?            │
│  否(eden 满)               │
│  ▼                           │
│  Minor GC → Survivor 区复制  │
│  晋升判断 → 老年代分配       │
└─────────────┬───────────────┘

        初始化 + 返回引用

本节小结

对象分配看似简单,背后是一套完整的判断流程:

步骤判断条件结果
TLAB 分配线程自己的 TLAB 有空间快速分配,无需同步
Eden 指针碰撞Eden 有足够空间正常分配
Survivor 转移Minor GC 后 Survivor 有空间存活对象进入 Survivor
老年代担保老年代空间足够Minor GC 后晋升
老年代直接分配对象很大直接在老年代分配
OOM全部失败OutOfMemoryError

理解分配过程,是理解 GC 行为的入口。下一节,我们来看 TLAB 与内存分配策略

基于 VitePress 构建