第一章 GC基础概念:什么是GC,为什么需要GC

大家好,我是你们的老朋友。做Java这么多年,GC可以说是我又爱又恨的东西。爱它省事儿,恨它起来能把线上服务搞崩溃。今天我们就从头聊一聊GC——到底是什么,为什么必须有它。

1.1 什么是GC

GC全称Garbage Collection,翻译过来就是“垃圾回收”。在Java里,它是个后台线程,自动帮我们清扫不再使用的对象。

我刚开始写C++的时候,每次new完都要小心翼翼地delete。忘了释放内存?泄漏。多释放了?崩溃。后来转到Java,第一次听说不用管内存释放,我还觉得不靠谱——“哪有这么好的事?”

结果呢?确实有。JVM的GC就是那个替我们擦屁股的人。

说白了,GC干三件事:

  • 标记:找出哪些对象还活着,哪些已经死了
  • 清除/整理:把死对象的内存回收
  • 压缩:把活对象挪到一起去,避免内存碎片

举个例子,你写个List,往里塞了一堆数据,用完不删——GC会判断这些对象是不是还“可达”。如果没人引用它们,就是垃圾。

核心一句话:GC管理的对象都在堆(Heap)上。栈上的局部变量和方法调用,不用GC管——方法结束就自动释放了。

1.2 为什么需要GC

你可能会问:“手动管理内存不是更可控吗?”嗯,我当年也这么想。直到我在一个C++项目里debug内存泄漏,三天三夜没合眼……

  • 减少人为错误:忘记释放、释放两次、释放后继续使用——全是坑。GC把这些坑填上大半。
  • 提高开发效率:你只管new对象,GC帮你兜底。尤其现代应用动不动几百个线程,手动管理?想都不敢想。
  • 应对复杂对象图:一个对象引用另一个对象,循环引用也很常见。手动管理循环引用简直是噩梦,GC通过可达性分析轻松搞定。

我个人习惯把GC理解为“内存管家”。你住房子(堆),管家帮你打扫卫生。你要是乱扔垃圾(创建对象不清理),管家就勤快点;你要是规规矩矩,管家就偷个懒。

1.3 GC的核心模型:分代收集

大多数商用GC(包括HotSpot JVM)都采用分代收集理论。为什么?因为经过大量统计发现:

  • 大多数对象“朝生夕死”,创建完很快就用不到了
  • 少数对象“长命百岁”,越活越老

JVM把堆划分成几个区域:

区域 说明 GC频率
新生代(Young Gen) 存放刚创建的对象,分为Eden、Survivor0、Survivor1 频繁,Minor GC
老年代(Old Gen) 存放经历过多次GC依然存活的对象 较低,Major GC / Full GC
元空间(Metaspace) 存放类元数据、常量池等,不再用永久代 类似老年代

我遇到过不少案例,新生代对象拼命创建,GC频繁触发,一看代码就是个临时对象没被引用——还没被回收?哦,被某个长生命周期的单例对象给持有了。这就是典型的“后台任务未释放引用”导致的GC抖动。

一个小技巧:如果你发现Young GC很频繁,但老年代占用却很低,往往是对象分配速度太快。反过来,如果老年代持续增长,就要警惕内存泄漏或者大对象直接进入老年代。

1.4 可达性分析——GC判断对象生死的方法

GC不是靠引用计数,而是从一组根(GC Roots)出发,遍历所有可达的对象。如果某个对象从根出发“不可达”,那就是垃圾。

GC Roots包括:

  • 栈帧中的局部变量(方法参数、局部变量)
  • 静态字段引用的对象
  • JNI的全局引用
  • 活跃线程对象

嗯,这里要注意:如果你的后台任务线程持有了一个本该释放的大对象引用,哪怕任务逻辑结束,只要线程还在运行(比如在线程池里),这个对象就始终是“可达”的!

// 典型的后台任务泄漏场景
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
    // 这里创建了一个超大的byte数组
    byte[] hugeData = new byte[100 * 1024 * 1024];
    // 处理完数据后,hugeData没有被置为null
    doSomething(hugeData);
    // 线程结束?不,线程被线程池回收,但栈帧中的hugeData引用在lambda闭包里没释放
    // 导致整个数组GC无法回收
});

你会觉得,“函数执行完了,局部变量不就自动释放了吗?”错!Lambda表达式的闭包会把外部变量捕获到对象里,如果这个对象一直活着(比如线程池的核心线程),那hugeData的引用也一直活着。

我曾经排过的坑:在线支付系统的批量导入任务中,发现Minor GC每秒钟触发30多次,CPU飙升。dump堆一看,全是byte[]被一个线程池的Worker线程引用着。原因就是后台任务里创建了临时缓冲区,任务结束忘记置null,而线程池的线程是常驻的。——那次排查花了我整整一个下午。

1.5 本章知识体系总览

下面这张图帮你捋清楚GC的基础脉络。我从根节点开始,一步步拆解GC的“为什么”、“是什么”、“怎么做”。

GC基础概念知识体系 GC:自动垃圾回收 为什么需要GC? 减少人为内存错误 提高开发效率 应对复杂对象图/循环引用 核心机制 分代收集(Young/Old) 可达性分析(GC Roots) 标记-清除 / 标记-整理 GC抖动问题 后台任务未释放引用 → 对象无法回收 → 频繁GC 核心思路:理解GC机制 → 识别引用泄漏 → 定位后台任务 本课程第1章铺垫基础,后续深入排查与优化实战 💡 个人经验: 我记得最早接触GC抖动是在一个秒杀系统里,后台统计任务每5分钟执行一次,每次创建一个Map存大量数据, 但Map被一个全局静态变量引用着。结果老年代不断膨胀,Full GC一次要好几秒——那个时候才真正意识到“未释放引用”的威力。

1.6 理解GC对排查抖动的重要性

你想想看,如果一个后台任务创建了1GB的临时数据,任务跑完后你不清理引用——GC就永远搞不走这1GB。堆越来越满,GC频率越来越高,最终导致服务卡顿。

具体到我们的课程主题《后台任务未释放引用GC抖动排查指南》,GC的基础概念是必须掌握的武器。没有这个基础,你连GC日志都看不懂。

本章小结:

  • GC自动管理堆内存,通过可达性分析判断对象生死
  • 分代收集是主流策略:新生代频繁Minor GC,老年代较少Major GC
  • 后台任务如果持有对象引用不释放,会导致对象老年代化,引发GC抖动
  • GC Roots包括栈帧局部变量、静态字段等——线程池里的线程就是潜在“根”