第一章 GC基础概念:什么是GC,为什么需要GC
大家好,我是你们的老朋友。做Java这么多年,GC可以说是我又爱又恨的东西。爱它省事儿,恨它起来能把线上服务搞崩溃。今天我们就从头聊一聊GC——到底是什么,为什么必须有它。
1.1 什么是GC
GC全称Garbage Collection,翻译过来就是“垃圾回收”。在Java里,它是个后台线程,自动帮我们清扫不再使用的对象。
我刚开始写C++的时候,每次new完都要小心翼翼地delete。忘了释放内存?泄漏。多释放了?崩溃。后来转到Java,第一次听说不用管内存释放,我还觉得不靠谱——“哪有这么好的事?”
结果呢?确实有。JVM的GC就是那个替我们擦屁股的人。
说白了,GC干三件事:
- 标记:找出哪些对象还活着,哪些已经死了
- 清除/整理:把死对象的内存回收
- 压缩:把活对象挪到一起去,避免内存碎片
举个例子,你写个List,往里塞了一堆数据,用完不删——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抖动。
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的引用也一直活着。
1.5 本章知识体系总览
下面这张图帮你捋清楚GC的基础脉络。我从根节点开始,一步步拆解GC的“为什么”、“是什么”、“怎么做”。
1.6 理解GC对排查抖动的重要性
你想想看,如果一个后台任务创建了1GB的临时数据,任务跑完后你不清理引用——GC就永远搞不走这1GB。堆越来越满,GC频率越来越高,最终导致服务卡顿。
具体到我们的课程主题《后台任务未释放引用GC抖动排查指南》,GC的基础概念是必须掌握的武器。没有这个基础,你连GC日志都看不懂。
本章小结:
- GC自动管理堆内存,通过可达性分析判断对象生死
- 分代收集是主流策略:新生代频繁Minor GC,老年代较少Major GC
- 后台任务如果持有对象引用不释放,会导致对象老年代化,引发GC抖动
- GC Roots包括栈帧局部变量、静态字段等——线程池里的线程就是潜在“根”