第1章 GC基础概念:垃圾回收原理、分代与常见算法

各位好,我是你们的老朋友。做后台开发这么多年,我遇到过最头疼的问题之一就是GC抖动和内存泄漏。记得有一次线上服务每隔几分钟就卡一下,查了半天才发现是GC参数没调好。从这一章开始,我们先把地基打牢——聊聊垃圾回收的基本原理、分代收集以及三种经典算法。

1.1 垃圾回收到底在回收什么?

先说个最基础的问题:哪些对象算是“垃圾”?很简单,程序运行过程中,那些再也无法被引用的对象就是垃圾。比如你在方法里 new 了一个临时对象,方法执行完就没人用它了,那它就该被回收。

JVM 怎么判断对象是不是“活着”?主要靠可达性分析。从一组称为“GC Roots”的根对象出发(比如栈帧中的局部变量、静态变量、JNI引用等),顺着引用链往下走。能到达的对象就是活的,到不了的就等着被清理。

避坑提醒:我曾经遇到过一个问题:一个对象明明不用了,但因为被一个长期存活的对象间接引用着,一直没法回收。后来查代码才发现是个ArrayList忘了清空。所以,别小看可达性分析,根对象的定义直接影响内存泄漏的判断。

1.2 分代收集:为什么要把堆分成两代?

其实是个很朴素的观察——大部分对象“朝生夕死”。你在for循环里new的临时对象,往往很快就不可达了。而少部分对象会长期存活,比如缓存对象、全局配置。所以JVM把堆分成新生代(Young)和老年代(Old),分别用不同的策略处理。

  • 新生代:对象刚出生的地方。GC 频率高,但每次收集快。占堆的大约1/3。
  • 老年代:经过多次GC还活着的对象会晋升到这里。GC 频率低,但单次开销大。

分代的核心思想:对不同特性的对象用不同的算法,达到吞吐量和延迟的平衡。你看,设计者还是挺聪明的。

分代特点GC频率常见算法
新生代对象多,死得快高(Minor GC)复制算法
老年代对象少,常存活低(Major GC / Full GC)标记-清除 / 标记-整理
我个人习惯:调优时先关注新生代大小。如果Minor GC过于频繁,多半是新生代太小,或者有大对象直接进了老年代。举个真实案例:有一次我们的系统每秒产生大量临时对象,Minor GC达到每秒几十次,CPU飙升。调大新生代后直接降到了每两秒一次。

1.3 三种经典GC算法,各自什么脾气?

1.3.1 标记-清除(Mark-Sweep)

算法分两步:先标记所有可达对象,然后清理掉没被标记的。听起来简单,但有两个大问题:

  1. 产生内存碎片 —— 清理后的空间不连续,下次分配大对象时可能找不到足够大的连续块,导致提前触发GC。
  2. 效率不稳定 —— 随着对象越来越多,标记和清理的时间都会增长。

我在一个日志系统里见过被碎片坑惨的案例:老年代空闲空间还有40%,但因为碎片严重,一个大对象分配失败,直接触发了Full GC。嗯,标记-清除虽然实现简单,但生产环境很少单独用。

1.3.2 复制算法(Copying)

把可用内存分成两块,每次只使用其中一块。GC时,把存活的对象复制到另一块,然后清空当前块。完美解决碎片问题,而且效率高(因为大多数对象都是死的,复制工作少)。

缺点也很明显:浪费了一半内存。不过,新生代的对象死亡率高,浪费一点可以接受。所以HotSpot的新生代默认采用这种算法,并且把空间划分为一个较大的Eden区和两个较小的Survivor区,比例通常是8:1:1。这样实际浪费只有10%。

注意:复制算法不适合老年代。因为老年代对象存活率高,复制成本太大,而且需要额外内存做“备用区”。你想想看,如果老年代也用复制,那得预留多少空间?性价比太低了。

1.3.3 标记-整理(Mark-Compact)

结合了标记-清除和复制算法的优点。先标记,然后把存活对象向一端移动,整理成连续空间,再清理掉边界之外的内存。既有复制算法消除碎片的优点,又不需要额外内存。

但整理阶段需要移动大量对象,还要更新所有引用,停顿时间会比较长。所以,标记-整理主要用在老年代,而且通常配合低延迟的垃圾回收器(比如CMS的并发标记阶段之后做一次整理)。我参与过一个金融交易系统,要求GC停顿不能超过50ms,当时我们就是用的G1收集器(底层就是标记-整理思想),配合调优后效果不错。

算法优点缺点适用代
标记-清除实现简单,不移动对象内存碎片,效率不稳定老年代(很少单独用)
复制无碎片,效率高浪费空间(新生代可接受)新生代
标记-整理无碎片,内存利用率高移动对象慢,停顿大老年代

1.4 本知识体系一览

下面是本章核心概念的框架图,把GC原理、分代和算法串在一起。你可以把它当作一张“地图”,后面讲具体收集器时,会反复引用这些基础。

GC基础概念体系 GC原理 可达性分析 + GC Roots 分代收集 新生代 / 老年代 标记-清除 有碎片,仅标记+清理 复制算法 适合新生代,无碎片 标记-整理 适合老年代,无碎片 对象从新生代晋升到老年代 (经过多次Minor GC存活) 老年代GC通常会触发标记-整理 或标记-清除(视收集器而定) 基础决定上层,后面讲GC调优都离不开这些概念
一个小思考:看了上面的图,你有没有发现一个规律?算法选择本质上是在“时间”、“空间”、“停顿时间”三者之间做权衡。复制算法用空间换时间(新生代),标记-整理用时间换空间(老年代),而标记-清除看上去省了移动对象的开销,但碎片问题又会反噬性能。我在实际中更推荐优先使用G1或者ZGC,它们内部都融合了多种算法的思想。

1.5 本章小结

GC的基础就三个事:怎么判断垃圾、怎么分代、怎么回收。不用急着背算法细节,先理解为什么需要分代、三种算法各自对应的场景。下一段路会更精彩——我们会把这些概念用到线上故障分析中。嗯,先消化今天的内容,我们后面聊。