Appearance
对象生命周期:从 new 到回收
对象生命周期把堆、栈、类加载、对象头、引用链和 GC 串在一起。理解这条链路后,堆 OOM、Full GC、对象晋升、大对象、ThreadLocal 泄漏和缓存问题会更容易定位。
总链路
text
new 指令
-> 类加载检查
-> 分配内存
-> 初始化对象头
-> 执行构造方法
-> 被引用持有
-> 参与 GC Roots 可达性分析
-> 存活 / 移动 / 晋升 / 回收类加载检查
执行 new 前,JVM 要确认目标类已经被加载、链接和初始化。如果类还没准备好,会触发类加载流程。
这就是为什么第一次创建某个类的对象可能比后续更慢:它可能包含类加载、验证、初始化、静态块执行和 JIT 预热。
内存分配
对象通常分配在堆里。常见方式:
| 方式 | 说明 |
|---|---|
| 指针碰撞 | 空闲空间连续时,移动指针即可分配 |
| 空闲列表 | 空闲空间不连续时,从列表中找可用块 |
| TLAB | 每个线程预分配一小块 Eden,减少多线程分配竞争 |
TLAB(Thread Local Allocation Buffer)让多数小对象分配非常快。对象分配并不总是昂贵,真正昂贵的是大量对象存活、晋升或造成 GC 压力。
对象头
HotSpot 对象通常包含:
| 部分 | 说明 |
|---|---|
| Mark Word | 哈希、锁状态、GC 年龄等 |
| Klass Pointer | 指向类元数据 |
| Array Length | 数组对象才有 |
| 实例数据 | 字段值 |
| 对齐填充 | 满足内存对齐 |
所以对象大小不等于字段大小。引用压缩、字段排列、对象头、数组头和对齐都会影响实际内存。
构造方法
对象内存分配后,JVM 会执行初始化逻辑:
text
默认零值 -> 字段初始化 -> 构造代码块 -> 构造方法构造方法里如果把 this 暴露给其他线程,可能出现对象尚未完全初始化就被访问的并发问题。
引用链
对象是否能回收,关键看是否可达:
text
GC Roots -> 引用 -> 引用 -> 目标对象常见让对象长期存活的引用:
- 静态集合。
- 本地缓存。
- ThreadLocal。
- 线程池任务队列。
- 监听器、回调、订阅关系。
- 类加载器。
- JNI 引用。
内存泄漏的本质不是“没有 free”,而是“不该继续被引用的对象仍然可达”。
年轻代、老年代和晋升
传统分代理解仍然有用:
text
Eden -> Survivor -> Old常见过程:
- 新对象进入 Eden。
- Young GC 后仍存活的对象进入 Survivor。
- 对象年龄增加。
- 达到阈值或 Survivor 放不下时晋升到 Old。
- 老年代压力过大时触发 Mixed GC 或 Full GC。
G1、ZGC 等回收器内部模型不同,但仍然围绕对象生命周期、存活率和回收收益做判断。
大对象
大对象可能直接进入老年代,或者在 G1 中成为 Humongous 对象。
常见来源:
- 大数组。
- 大字符串。
- 大 JSON。
- 一次性读取完整文件。
- 批量查询结果。
- 图片、二进制内容。
优化方向:
- 分块处理。
- 流式读取。
- 限制单批大小。
- 避免长期缓存大对象。
- 对外部内容使用对象存储或临时文件。
ThreadLocal 泄漏
ThreadLocal 典型引用关系:
text
Thread -> ThreadLocalMap -> Entry -> value在线程池中,线程长期存活。如果业务没有及时 remove(),value 也可能长期存活。
建议:
java
try {
CONTEXT.set(value);
// business
} finally {
CONTEXT.remove();
}从对象生命周期排查 OOM
- 看 GC 日志确认是堆、元空间、直接内存还是容器 OOM。
- 如果是堆,抓 heap dump。
- 看最大对象和最多对象。
- 找 GC Roots 引用链。
- 判断是容量不足、峰值流量、缓存无界、队列积压还是泄漏。
- 先修生命周期和上限,再考虑调大堆。
常见误区
| 误区 | 更准确的理解 |
|---|---|
| 对象创建一定慢 | TLAB 下小对象分配很快,问题通常在存活和回收 |
| OOM 就是堆太小 | 可能是泄漏、直接内存、元空间、线程栈或容器限制 |
| Full GC 能解决泄漏 | 可达对象不会被 GC 回收 |
| 缓存越大越好 | 缓存必须有容量上限、过期策略和监控 |
| heap dump 一定能看到全部内存 | 直接内存、native、线程栈不完整体现在 heap dump 中 |
