Java 垃圾回收

Java / JVM / GC2026.09.147 分钟阅读

对象可达性、G1 回收过程与 GC 日志。

对象什么时候能回收

一次请求结束后,对象仍可能被缓存、监听器或任务队列引用。JVM 从 GC Roots 出发沿引用关系查找对象,仍能通过普通强引用到达的对象不能作为垃圾回收。

GC Roots 包括活动线程中的引用、JNI 引用等;存活类的静态字段也可能保留对象。对象占着内存,可能是仍被引用,也可能是已不可达但尚未回收。前一种情况需要解除引用链,增加 GC 次数没有用。

import java.util.ArrayList;
import java.util.List;

public class RetainedDemo {
    static final List<byte[]> CACHE = new ArrayList<>();

    public static void main(String[] args) {
        byte[] payload = new byte[1024 * 1024];
        CACHE.add(payload);
        payload = null;
        System.out.println(CACHE.size()); // 1
        CACHE.clear(); // 解除列表对数组的引用,不保证立刻回收
    }
}

payload 与 CACHE 中的元素起初指向同一数组。把 payload 置为 null 后,存活类的静态字段 CACHE 仍通过列表持有数组。CACHE.clear() 解除这条路径后,没有其他引用的数组就可以被回收,具体时间由 JVM 决定。

示例可保存为 RetainedDemo.java,使用 Java 21 运行。排查堆增长时,除了数实例,还要沿 GC Roots 查找持有者。对象的浅大小只计算自身占用,一个很小的集合也可能保留大量数据。找到引用来源后,再检查缓存淘汰、任务结束和监听器注销是否及时。

G1 怎样回收堆空间

G1 将堆划分为多个 Region,分别用于年轻代和老年代。每一代无需占据连续的大块空间,回收时也可以只处理选中的一组 Region。

年轻代回收会将存活对象搬迁到其他区域,并释放已处理区域的空间。并发标记提供老年代区域的存活信息,后续 Mixed GC 可以同时选择年轻代和部分老年代区域。区域选择需要综合存活量、预期回收收益和暂停时间目标。

其他 Region 中的对象可能引用待回收区域里的对象,G1 需要记录这些跨区域引用。对象关系越复杂、引用更新越频繁,维护记录的开销也会增加。

达到 G1 大对象阈值的分配会走 humongous 对象路径。频繁创建大型数组或字符串时,可以结合大对象日志查找分配位置。调整 Region 大小会同时改变区域数量和大对象分类。

并发与停顿

垃圾收集期间,业务线程可能持续修改对象引用。收集器必须借助屏障、记录与同步阶段维持正确的存活视图。部分工作可以并发进行,另一些阶段仍需暂停业务线程;并发执行的工作也会消耗 CPU,并与业务竞争资源。

G1 搬迁存活对象时需要复制数据并修正引用,因此存活量、对象关系和可用处理资源都会影响暂停时间。同样的堆容量下,大量短命对象与较大的长期存活集具有不同的回收特征。较大的堆可以提供更多空间余量,但不能直接消除对象保留问题。

G1 用停顿目标安排回收工作,但不保证每次暂停都低于目标。降低目标可能改变区域选择和回收频率,调节后要观察停顿分布、回收频率及吞吐量。

看 GC 日志

先把 GC 事件与业务延迟的时间对齐。以下配置启用 JDK 21 的 G1 日志,并设置文件轮转:

# JDK 21;在可写目录运行自己的应用
java -XX:+UseG1GC -Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=20M -jar app.jar
# 以下是教学示意行,不是实测记录
[2.100s][info][gc] GC(3) Pause Young (Normal) (G1 Evacuation Pause) 64M->12M(128M) 4.200ms

示意行是一次年轻代 evacuation 停顿:总堆使用量从 64M 降到 12M,堆容量为 128M,暂停 4.200ms。判断是否持续保留对象,还要看后续多次回收后的占用。

连续回收后的占用趋势可以帮助区分问题。占用持续抬高时,应检查存活集及其引用来源;存活集稳定但回收频繁时,应进一步分析分配速率。并发周期无法及时完成或出现 evacuation 失败时,还需结合空间余量、CPU 供给和完整阶段日志判断。

业务延迟与 GC 事件对不上时,继续检查线程阻塞和外部依赖。需要追踪对象保留链时可以采集堆转储;采集会占用磁盘和运行时间,文件也可能包含业务数据。

比较参数时,固定输入、容器限制和可用处理器数,观察多个稳定回收周期。长期持有对象就修复生命周期,分配过快就检查对象创建路径,空间或 CPU 不足则调整容量与回收目标。

— 本页完 —
← 返回文章列表

搜索内容

输入关键词搜索文章、笔记与随笔。