一、Java 堆内存概述
Java 虚拟机(JVM)的堆内存是用于存储对象和数组的运行时数据区域。以 HotSpot JVM 为例,堆内存通常分为以下几个部分:
- 年轻代:包括 Eden 区和两个 Survivor 区(S0 和 S1),用于存放新创建的对象。
- 老年代:存储生命周期较长的对象。
- 元空间:存储类元数据、方法信息等,取代了 Java 8 前的永久代。
对象分配主要发生在年轻代的 Eden 区,这是我们关注的重点。
二、对象在 Eden 区分配
当我们使用 new 关键字创建对象时,例如:
Object obj = new Object();
JVM 会执行以下步骤:
-
确定内存大小:JVM 计算对象所需内存,包括对象头(存储类信息、哈希码等)、实例变量和对齐填充(通常对齐到 8 字节的倍数)。
-
分配内存:
优先使用 TLAB:JVM 为每个线程分配一个线程本地分配缓冲区,对象优先在 TLAB 中分配,避免多线程竞争堆锁,提高效率。
Eden 区分配:如果 TLAB 空间不足,对象在 Eden 区分配。JVM 使用 指针碰撞(连续内存时移动指针)或 空闲列表(碎片化内存时选择合适块)分配内存。 -
初始化:
清零内存:分配的内存空间初始化为默认值(例如,0、null)。
设置对象头:写入类元数据、分代年龄等信息。
调用构造函数:执行 方法,完成对象初始化。
三、Eden 区满后触发 Minor GC
当 Eden 区空间不足以分配新对象时,JVM 触发 Minor GC(也叫 Young GC,YGC),清理年轻代中的无用对象。过程如下:
-
标记存活对象:扫描 Eden 区和 From Survivor 区(S0 或 S1)的存活对象。
-
复制存活对象:
存活对象从 Eden 区和 From Survivor 区复制到 To Survivor 区(S1 或 S0)。
如果 To Survivor 区空间不足,部分对象会直接晋升到老年代。 -
清理内存:清空 Eden 区和 From Survivor 区,释放无引用对象的空间。
-
角色互换:From Survivor 和 To Survivor 区角色互换,为下一次 Minor GC 做准备。
分代年龄:每次 Minor GC,存活对象的分代年龄(GC Age)加 1。当年龄达到阈值(默认 15,可通过 -XX:MaxTenuringThreshold 配置)时,对象晋升到老年代。
四、后续内存管理
Minor GC 后,Eden 区腾出空间,新对象继续在 Eden 区分配。以下是后续的内存管理机制:
- 对象晋升老年代:
- 大对象:超过一定大小(通过 -XX:PretenureSizeThreshold 设置)的对象直接分配到老年代,避免频繁复制。
- Survivor 区不足:To Survivor 区无法容纳所有存活对象时,部分对象晋升到老年代。
- 年龄阈值:对象经过多次 Minor GC 后,年龄达到阈值,晋升到老年代。
- 老年代满后触发 Full GC:
当老年代空间不足时,JVM 触发 Full GC,清理整个堆(年轻代 + 老年代)。
Full GC 使用 标记-清除-整理算法,回收无用对象并整理内存碎片,耗时较长,可能导致应用暂停(Stop-The-World)。 - 内存分配失败:
如果 Full GC 后仍无足够空间,JVM 抛出 OutOfMemoryError。
五、总结
Java 对象的堆内存分配和垃圾回收是一个高效且复杂的过程。对象通常在 Eden 区分配,Eden 区满后触发 Minor GC,存活对象移动到 Survivor 区,多次 GC 后可能晋升到老年代,最终通过 Full GC 清理整个堆。