堆内存深度解析
堆是 JVM 中最大的内存区域,所有对象实例和数组在这里分配,垃圾回收也主要发生在这里。理解堆的结构与分配流程,是排查内存问题、配置 JVM 参数的基础。
堆的逻辑分区
堆在逻辑上分为新生代(Young)和老年代(Old),新生代内部又分为 Eden、Survivor0、Survivor1 三个区。
Heap
├── 新生代 Young(默认约占堆的 1/3)
│ ├── Eden 新对象优先分配
│ ├── Survivor0 存活对象复制区
│ └── Survivor1 两个 Survivor 区轮流使用
└── 老年代 Old 长期存活的大对象这种分代设计的依据是弱代假设:绝大多数对象朝生夕灭,只有少量对象长期存活。新生代用复制算法快速回收短命对象,老年代用标记-整理算法处理长命对象,兼顾吞吐与内存利用率。
分区大小配置
| 参数 | 作用 |
|---|---|
-Xmn256m | 指定新生代大小 |
-XX:SurvivorRatio=8 | Eden:S0:S1 = 8:1:1 |
-XX:NewRatio=2 | 老年代:新生代 = 2:1 |
新生代并非越大越好。它越大,老年代就越小,导致大对象放不下、老年代频繁 Full GC;它越小,Minor GC 越频繁。通常根据对象存活率与 GC 日志动态调整。
对象分配流程
new 对象
│
├─ 是否栈上分配(逃逸分析,标量替换)→ 是 → 栈上分配,不进入堆
│
├─ 是否 TLAB 有空间 → 是 → 在 TLAB 内分配(无锁)
│
├─ 是否大对象(> -XX:PretenureSizeThreshold)→ 是 → 直接进入老年代
│
└─ 否则 → 在 Eden 分配
│
└─ Minor GC 后存活 → 年龄 +1,S0/S1 复制 → 年龄达阈值 → 晋升老年代关键点:
- 逃逸分析:方法内新建、未逃逸出方法作用域的对象,JIT 可能通过标量替换将其拆散到栈上,避免堆分配。
- TLAB 分配:多数情况下对象优先在 Eden 的 TLAB 中分配,免锁、快。
- 大对象直接进老年代:超过
-XX:PretenureSizeThreshold(仅 Serial/ParNew 生效)的大对象直接分配在老年代,避免在新生代反复复制。 - 年龄晋升:对象每经历一次 Minor GC 存活则年龄 +1,默认年龄到 15(
-XX:MaxTenuringThreshold)晋升老年代;动态年龄判定:S 区中同龄对象总和超一半时,按年龄从小到大晋升。
TLAB 本地线程分配缓冲
TLAB(Thread Local Allocation Buffer)是 Eden 中为每个线程划分的一小块私有分配区,避免多线程并发分配时的锁竞争。
Eden
├── Thread1 的 TLAB
├── Thread2 的 TLAB
└── Thread3 的 TLAB- 默认开启:
-XX:+UseTLAB - 默认大小约为 Eden 的 1%,可用
-XX:TLABSize指定 - TLAB 不足时,对象直接在 Eden 中分配;TLAB 空间浪费可通过
-XX:TLABWasteTargetPercent控制(默认 1%)
TLAB 只负责分配,不负责回收——TLAB 内的对象仍按普通对象参与 GC。它解决的是"分配"的并发问题,不是"回收"问题。
对象头与对象布局
一个 Java 对象在堆中的布局分为三部分:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。
┌────────────────────── 对象头(12/16 字节) ──────────────────────┐
│ Mark Word(8 字节) 存储哈希、GC 分代年龄、锁状态标志位 │
│ Klass Pointer(4/8 字节) 指向方法区中的类元数据(类型指针) │
│ 数组长度(仅数组对象,4 字节) │
└───────────────────────────────────────────────────────────────┘Mark Word 的多状态复用
Mark Word 是一块可复用的空间,同一时刻只有一种含义:
| 状态 | 存储内容 |
|---|---|
| 无锁 | 对象哈希、分代年龄 |
| 偏向锁 | 线程 ID、Epoch、分代年龄 |
| 轻量级锁 | 指向栈中锁记录的指针 |
| 重量级锁 | 指向 Monitor 的指针 |
| GC 标记 | 空 |
指针压缩
64 位虚拟机中普通对象指针占 8 字节。开启 -XX:+UseCompressedOops 后,堆内指针压缩为 4 字节,配合 8 字节对齐(对象大小按 8 字节补齐),可用 4 字节表达 32GB 内的地址。
- 默认开启,堆小于 32GB 时生效
- 好处:对象头更小,缓存利用率更高,内存占用下降约 10-20%
- 注意:
-Xmx超过 32GB 时压缩自动关闭,对象头变大,可能反而更费内存
常见问题排查
- Eden 频繁打满:观察 Minor GC 间隔是否过短,检查是否有大对象、TLAB 分配占比、逃逸分析是否生效。
- 大对象直达老年代:用
-XX:PretenureSizeThreshold约束,或优化代码避免一次性创建超大数组。 - 对象晋升过快:排查
MaxTenuringThreshold是否被动态年龄判定提前覆盖,S 区容量是否过小。 - 堆参数合理性:以
jstat -gc输出为准,看新生代分配速率与晋升速率,再决定-Xmn的调整方向。