臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

GC(Garbage Collection,垃圾回收)是 JVM 的自动内存管理机制。对象变得不可达时,只是具备回收资格;通常还要等 JVM 因内存分配压力、代空间占用等条件触发相应的 GC,内存才可能真正被回收。

如果线上表现为 GC 线程活跃并伴随 CPU 升高,可继续阅读:../工作实践/线上服务 CPU 使用率过高排查。

二、🧭 理论:GC 是什么

2.1 GC 的职责

Java 对象通常创建在堆中。对象越来越多,堆空间会逐渐被占满;GC 会判断哪些对象还活着,回收无用对象占用的内存,必要时整理内存空间。

可以把堆想成仓库、对象想成货物,GC 就是清理仓库的管理员:还在使用的货物保留,已经不用的货物清走。

类比边界

真实的 GC 由 JVM 执行,并依据对象之间的引用关系判断对象是否仍然存活。

2.2 GC 解决的问题

  • 开发者通常不需要手动释放对象内存。
  • 无用对象占用的内存可以再次使用。
  • 降低忘记释放或重复释放内存的风险。

2.3 GC 管理哪些内存

启动一个 Java 程序后,操作系统会给这个 JVM 进程分配内存。这些内存不全属于 Java 堆,可以简单分成下面几块:

内存区域大白话理解GC 是否主要负责
Java 堆存放 new 出来的对象是,GC 的主要工作区域
Metaspace存放已经加载的类的信息特定条件下可以卸载无用的类
线程栈每个线程执行方法时使用的工作空间线程结束后释放,不靠普通对象回收
直接内存和本地内存Java 堆外的缓冲区、本地库等使用的内存不属于普通的堆对象回收

可以把 JVM 进程想成一套房子:

JVM 进程总内存
├── Java 堆:主要放对象,GC 重点清理这里
├── Metaspace:放类的信息
├── 线程栈:每个线程自己的工作空间
└── 直接内存等:堆外使用的其他空间

例如,操作系统显示 Java 进程用了 2 GB,但 Java 堆只用了 800 MB,剩下的内存可能被线程栈、Metaspace 或直接内存占用。因此:

记住这一句

进程内存高,不一定是 Java 堆高;堆不高时,频繁执行 GC 也不一定能把进程内存降下来。

三、⚙️ 对象不可达后会立即回收吗

3.1 不会立即回收

“不可达”和“已经回收”是两个不同状态:

对象仍可达
   ↓ 引用断开
对象不可达,只具备回收资格
   ↓ JVM 触发并完成相应 GC
对象占用的内存被回收或整理后复用

JVM 通常不会因为某一个对象刚变得不可达,就立刻单独清理它。GC 是按一批内存区域统一处理的,具体触发条件和执行方式取决于垃圾收集器。

对象如何被判定为不可达,继续看:16.GC如何判断对象可以被回收。

3.2 一次 GC 的简化过程

  1. 业务线程不断创建对象,消耗堆空间。
  2. 某个内存区域接近分配极限,或达到收集器设定的启动条件。
  3. JVM 执行 GC;部分阶段会暂停业务线程,也就是 Stop-The-World(STW)。
  4. GC 找出存活对象,回收不可达对象占用的空间。
  5. 存活对象可能被复制、晋升或移动,应用随后继续运行。

注意

System.gc() 只是建议 JVM 尽力执行垃圾回收,规范不保证回收某个对象、释放多少内存或何时完成。业务代码通常不应依赖它保证回收时机。

四、📦 YGC、FGC 和分代回收

4.1 为什么要分年轻代和老年代

大多数对象存活时间很短。HotSpot 利用这个特点,把堆按对象年龄划分为年轻代和老年代:

区域常见对象回收特点
年轻代刚创建、生命周期较短的对象回收频繁,通常回收比例高
老年代多次回收后仍存活或较大的对象回收频率较低,处理成本通常更高

4.2 YGC 是什么

YGC 通常指 Young GC,也常被称为 Minor GC。年轻代没有足够空间继续分配对象时,JVM 会收集年轻代;存活对象可能进入 Survivor 区,达到一定年龄或满足其他条件后晋升到老年代。

YGC 通常会产生短暂 STW。YGC 多不一定有问题,关键要看频率、停顿时间、回收比例和对象晋升速度。

4.3 FGC 是什么

FGC 通常指 Full GC,会处理整个堆,往往还伴随压缩整理和类卸载等工作,因此停顿通常比 YGC 更长。

常见诱因包括:

  • 老年代空间不足,存活对象无法及时安置。
  • 对象分配或晋升失败,普通回收无法腾出足够空间。
  • G1 的并发标记和空间回收赶不上对象分配速度。
  • 大对象需要连续空间,但当前布局无法满足。
  • Metaspace 达到压力阈值,JVM 尝试通过 GC 和类卸载释放空间;具体日志类型取决于收集器。
  • 应用、框架或外部工具请求 System.gc()。

术语边界

Minor GC、Major GC、Old GC 和 Full GC 并不是所有收集器都以完全相同的方式定义。G1 还会出现 Young、Concurrent Start、Mixed 和 Full 等阶段,排查时应以实际 GC 日志为准。

4.4 以 G1 为例理解触发条件

现代 HotSpot 服务端环境通常使用 G1。它不是简单地“年轻代满了就 YGC、老年代满了就 FGC”:

  • 年轻代分配压力会触发 Young GC。
  • 老年代占用达到 IHOP 启动条件后,G1 会启动并发标记。
  • 标记完成后,通过 Mixed GC 同时回收部分年轻代和老年代 Region。
  • 如果应用分配太快,G1 来不及完成并发回收,最坏情况下才退化为 Full GC。

五、📊 CPU、内存和 GC 的关联

5.1 常见现象怎么判断

现象可能原因重点观察
堆内存呈锯齿状,上升后又明显下降正常分配和回收GC 后内存能否回到稳定水平
YGC 很频繁,但每次停顿短、回收后很低对象创建速度快,或年轻代偏小分配速率、YGC 总耗时、晋升量
YGC 频繁且老年代持续上涨存活对象较多并不断晋升GC 后老年代占用、对象生命周期
FGC 频繁,但每次释放很少活跃数据过大、内存泄漏或堆太小FGC 后占用、堆转储中的引用链
CPU 很高,同时 GC 时间占比很高回收过于频繁或并发 GC 抢占 CPUGC 线程 CPU、分配速率、停顿频率
进程内存很高,但 Java 堆稳定可能是直接内存、线程栈或本地内存Native Memory、线程数、DirectBuffer
多次 FGC 后仍无法分配有效空间不足OutOfMemoryError 类型和堆转储

5.2 CPU 为什么会被 GC 拉高

GC 要扫描引用、复制或移动对象、更新引用关系。并行 GC 会使用多个线程;G1 的并发标记也会在业务运行时占用 CPU。

因此可能出现两种影响:

  • 停顿变长:业务线程被 STW 暂停,请求延迟上升。
  • CPU 竞争:并发 GC 没有完全暂停业务,但会与业务线程争抢 CPU,吞吐量下降。

5.3 内存越大越好吗

不一定。堆太小会导致 GC 频繁;堆过大虽然可能减少触发次数,但每次需要处理的存活对象可能更多,也会占用更多机器内存。

真正需要平衡的是:

  • 吞吐量:业务真正工作的时间占比。
  • 延迟:一次 GC 停顿多久。
  • 内存占用:堆和整个 JVM 进程用了多少内存。

六、🚀 实践:打开 GC 日志并判断问题

6.1 前置准备

下面以 JDK 9 及以上版本的统一日志参数为例。先确认运行环境和实际使用的垃圾收集器,不要直接照搬调优参数。

6.2 最小实践:制造分配压力

新建 GcLogDemo.java:

import java.util.ArrayList;
import java.util.List;
 
public class GcLogDemo {
    // volatile 字段保留当前批次,避免对象在使用前被过早优化掉。
    private static volatile Object currentBatch;
 
    public static void main(String[] args) throws InterruptedException {
        for (int round = 0; round < 30; round++) {
            List<byte[]> batch = new ArrayList<>();
 
            // 每轮创建约 10 MB 对象,持续给年轻代制造分配压力。
            for (int i = 0; i < 40; i++) {
                batch.add(new byte[256 * 1024]);
            }
 
            // 新批次会替换旧批次;旧批次随后具备被回收的条件。
            currentBatch = batch;
            Thread.sleep(20);
        }
 
        System.out.println("分配完成");
    }
}

编译并使用 64 MB 堆运行:

javac GcLogDemo.java
java -Xms64m -Xmx64m -Xlog:gc* GcLogDemo

通常可以看到多条 Young GC 日志;具体次数、内存数值和停顿时间由 JDK、收集器与运行环境决定。这个例子验证的是“对象先失去引用,之后在内存分配压力触发的 GC 中被批量处理”。

6.3 在实际应用中记录 GC 日志

java \
  -Xms512m \
  -Xmx512m \
  -Xlog:gc*,safepoint:file=gc.log:time,uptime,level,tags \
  -jar app.jar
  • -Xms512m:设置初始堆大小。
  • -Xmx512m:设置最大堆大小。
  • -Xlog:gc*,safepoint...:把 GC 和安全点信息写入 gc.log。
  • app.jar:替换为实际应用文件。

6.4 看懂一条日志

下面是用于理解格式的简化示例,不是固定输出:

GC(12) Pause Young (Normal) (G1 Evacuation Pause) 400M->80M(512M) 8.5ms

它表示:

  • 这是第 12 次 GC。
  • 类型是 Young GC。
  • 堆占用从 400 MB 降到 80 MB。
  • 当前堆容量为 512 MB。
  • 本次停顿约 8.5 毫秒。

不要只看“发生了多少次 GC”,至少同时关注:

  1. GC 频率和累计耗时。
  2. 单次最大停顿时间。
  3. GC 前后释放了多少内存。
  4. GC 后老年代占用是否持续上升。
  5. CPU 升高的时间是否与 GC 日志重合。

6.5 推荐排查顺序

确认 JDK 与收集器
        ↓
查看 GC 类型、频率和停顿
        ↓
查看 GC 后堆与老年代占用
        ↓
区分分配过快、堆太小和对象无法释放
        ↓
再决定优化代码、调整堆或更换收集器

常见误区

不要看到 CPU 高就先增大堆,也不要看到 FGC 就立刻调整 JVM 参数。先用日志确认触发原因;如果对象一直被错误引用,单纯扩大堆只会推迟问题出现。

七、🎤 面试时怎么流利地说

7.1 回答路线

GC 的作用 → 不可达不等于立即回收 → 分代与触发条件 → YGC/FGC → CPU 和内存影响。

7.2 30 秒标准回答

GC 是 JVM 的自动内存管理机制,负责识别无用对象并回收堆内存。对象通过可达性分析被判断为不可达后,只是具备回收资格,不会立即回收;一般要等年轻代空间不足、老年代达到收集器阈值或出现分配压力时,JVM 才触发相应的 GC。YGC 主要处理年轻代,通常频繁但停顿较短;FGC 会处理整个堆,成本和停顿通常更高。排查问题时要结合 GC 频率、停顿时间、回收后的内存以及 CPU 占用一起看。

7.3 1—2 分钟展开回答

Java 对象通常先分配在年轻代。年轻代空间不足时会触发 Young GC,大部分短命对象在这里被回收,存活对象可能进入 Survivor 区或晋升老年代。以 G1 为例,老年代占用达到 IHOP 条件后会先启动并发标记,再通过 Mixed GC 回收部分老年代;如果回收赶不上分配、对象晋升或大对象分配失败,才可能退化成 Full GC。GC 会使用 CPU,STW 阶段还会直接造成请求停顿,所以不能只看次数:YGC 频繁但回收效果好未必异常;FGC 频繁且回收后内存仍很高,才更像活跃数据过大、堆过小或内存泄漏。

7.4 高频追问与短答

问:对象不可达后为什么不立即回收?

因为逐个对象立即回收的成本太高。JVM 会等内存区域达到收集条件后批量判断、复制、清理或整理对象。

问:YGC 很频繁一定有问题吗?

不一定。还要看每次停顿是否很短、回收比例是否足够高,以及总 GC 时间是否影响业务。

问:FGC 后内存为什么降不下来?

说明大部分对象仍然可达,可能是正常活跃数据太大,也可能是缓存、静态集合或线程等长期持有了不该保留的对象。

八、📌 总结

  • 对象不可达,只代表具备回收资格,不代表立即回收。
  • YGC 主要回收年轻代;FGC 通常处理整个堆,成本更高。
  • GC 一般由内存分配压力和收集器阈值驱动,具体规则取决于收集器。
  • CPU 高、内存高和 GC 频繁需要结合日志判断,不能单看一个指标。
  • 记忆链:对象分配 → 空间承压 → 触发 GC → 判断存活 → 回收或晋升 → 继续分配。

九、🔗 官方资料