JVMNotes

第 13 章:对象生命周期

zjc 于 2026-01-13 发布

这是《JVM 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 对象从分配、使用、不可达、回收到堆空间复用,贯穿 Young GC、晋升、并发标记和整理过程。理解生命周期,才能解释分配速率高、Survivor 溢出、老年代增长和缓存泄漏。

13.1 生命周期

Allocate
  -> Initialize
     -> Use
        -> Unreachable
           -> Collected
              -> Heap reused

各阶段职责:

阶段 责任方
分配 JVM,TLAB / Eden / 大 region
初始化 构造代码
使用 业务
不可达 GC 判定
回收 GC
释放 GC 与堆布局策略

13.2 分配速率

观察:

jstat -gc <pid> 1000

关注:

指标 含义
Eden delta 两次采样间 Eden 使用变化
YGC delta Young GC 次数
EU / OU Eden / Old 使用

粗略计算:

allocation rate ≈ Eden delta / sample interval

高分配速率会推高 Young GC 频率、CPU 消耗、缓存和内存带宽压力,并带来晋升压力。

常见来源:

  1. 日志字符串;
  2. DTO 重复转换;
  3. 大对象;
  4. 高频临时集合;
  5. JSON 序列化;
  6. 装箱。

13.3 Young GC

简化流程:

Eden 满
  -> 根扫描
     -> 复制存活对象到 Survivor / Old
        -> 清空 Eden 和空 Survivor

复制收集特点:

优点 代价
存活少时快 存活多时成本高
天然整理 复制对象
空间换时间 一块 Survivor 空闲

对象年龄参数:

-XX:MaxTenuringThreshold=15
-XX:TargetSurvivorRatio=50

HotSpot 可能根据 Survivor 压力动态调整晋升年龄,不一定等到 MaxTenuringThreshold。

13.4 晋升

常见晋升条件:

  1. 对象年龄达到阈值;
  2. Survivor 空间不足;
  3. 达到动态阈值;
  4. 大对象;
  5. GC 和堆布局策略。

观察:

jstat -gcutil <pid> 1000

风险:

现象 可能原因
Old 持续增长 缓存或泄漏
每轮 YGC 后 Old 增长 过早晋升
Survivor 高 存活对象多
Full GC 后 Old 仍高 长期存活对象

处理:

  1. 降低临时对象;
  2. 调整新生代;
  3. 优化缓存;
  4. 拆批;
  5. 选择更合适 GC;
  6. 用 GC 日志确认。

13.5 对象存活时间

朝生夕死对象
  -> Young 回收,成本低

中等生命周期对象
  -> Survivor 压力大,可能晋升

长期对象
  -> Old / concurrent marking

缓存对象
  -> 应有容量、TTL 和指标

Java 应用常存在弱分代假说:多数对象生命周期很短。但 Web 服务的 session、连接、缓存和框架对象会长期存活,因此需要分代和并发标记。

13.6 引用链

根对象包括:

  1. 线程栈局部变量;
  2. 静态字段;
  3. JNI 引用;
  4. 系统类加载器;
  5. 监视器对象;
  6. JVM 内部根。
GC Roots
  -> field
     -> object
        -> object
           -> cached object 不可回收

典型泄漏模式:

模式 示例
静态集合 static Map 无上限
监听器未注销 事件中心持有对象
ThreadLocal 线程池复用未清理
缓存 无 TTL / 容量
ClassLoader 热部署引用泄漏
Native 堆外未释放

13.7 ThreadLocal

private static final ThreadLocal<UserContext> CONTEXT =
        new ThreadLocal<>();

CONTEXT.set(new UserContext(userId));
try {
    process();
} finally {
    CONTEXT.remove();
}

线程池复用线程时,未 remove 会导致:

  1. 数据串用;
  2. ClassLoader 泄漏;
  3. 对象生命周期被拉长;
  4. 安全风险。

优先使用显式参数传递;确需上下文时必须成对清理。

13.8 Native 与堆外生命周期

堆内对象由 GC 回收,堆外资源必须显式释放或通过框架的引用计数处理。

try (ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024 * 1024)) {
    process(buffer);
}

Direct ByteBuffer 的释放时机不应依赖 GC;Netty 等框架有自己的引用计数:

ByteBuf buf = allocator.directBuffer();
try {
    process(buf);
} finally {
    buf.release();
}

堆外泄漏表现:

  1. heap 正常;
  2. RSS 增长;
  3. Direct buffer OOM;
  4. native memory 统计增长。

13.9 对象终止

finalize 已被废弃,不应使用。原因:

  1. 执行时机不确定;
  2. 可能 resurrection;
  3. 延迟对象回收;
  4. 异常被吞;
  5. 性能差。

替代:

try (Resource resource = open()) {
    process(resource);
}

需要响应堆回收时,优先重新设计生命周期,而不是依赖 finalize 或 Cleaner。

13.10 生产案例

案例一:Young GC 频繁

jstat -gcutil <pid> 1000

现象:YGC 每秒多次,Eden 快速填满。

处理:

  1. 用 allocation profile 找热点;
  2. 减少日志拼接;
  3. 复用对象;
  4. 修复深层查询;
  5. 评估新生代大小;
  6. 压测验证。

案例二:Old 缓慢增长

jstat -gcutil <pid> 10000
jcmd <pid> GC.heap_dump /tmp/app.hprof

MAT 分析:

  1. Dominator Tree;
  2. Top consumers;
  3. Path to GC Roots;
  4. 重复对象;
  5. ClassLoader。

本章小结

对象生命周期决定 GC 成本:朝生夕死对象适合 Young 复制,长期对象进入老年代,无上限缓存和未清理监听器会把生命周期意外拉长。堆内看可达性,堆外看显式释放。分析老年代问题要结合 GC 日志和堆 dump 引用链。

思考题

  1. 如何估算对象分配速率?
  2. 对象为什么会提前晋升?
  3. ThreadLocal 在线程池中有什么风险?
  4. 为什么不推荐 finalize?
  5. 如何分析 Old 区持续增长?