一、 垃圾回收的核心思想:可达性分析
垃圾回收(GC,Garbage Collection)到底是在回收什么?它的基本思想可以归纳为一句话:考察每一个对象的“可触及性”。
-
如何判断“活”的? JVM设定了一些“根对象(GC Roots)”(如栈帧中的本地变量、静态变量等)。从这些根节点出发,只要能够顺着引用链一路追踪到的对象,就说明它正在被使用,是活着的。
-
如何判断“死”的? 如果从所有的根节点出发,都无法访问到某个对象,说明这个对象已经“与世隔绝”,可以被当作垃圾回收了。
二、 四大核心垃圾回收算法演进
虚拟机并没有使用单一算法,而是根据不同场景采用了不同策略。我们来看看它们各自的优劣。
1. 引用计数法(被淘汰的“古老方法”)
这是最简单粗暴的方法:每个对象配一个计数器,有人引用就+1,引用失效就-1。计数器为0,就回收。

-
优点:实现极其简单,实时性好。
-
致命缺点:
-
无法解决“循环引用”(这是它被Java抛弃的根本原因)。比如对象A引用了B,B引用了A,但除此之外没别人引用它们了。此时A和B的计数器都是1,它们都认为对方还活着,导致垃圾无法被回收,造成内存泄漏。
-
性能消耗:每次赋值都要加减计数器,对系统性能有损耗。
结论:Java 虚拟机从未采用引用计数法。
2. 标记清除法(Mark-Sweep):现代算法的基础
这是最基础的算法,分为两个阶段:


标记:通过根节点,把能访问到的存活对象统统打上标记。
清除:把没有标记的对象直接抹掉,释放空间。
-
优点:不需要额外的空间,实现起来相对直接。
-
致命缺点:内存碎片化。
回收后的内存空间是不连续的。就像一块巨大的奶酪被挖得千疮百孔。如果你想要分配一个大对象(比如一个大数组),因为找不到连续的空间,明明总空间够,却分配失败,触发后续的垃圾回收,严重降低效率。
3. 复制算法(Copying):新生代的“救星”
既然标记清除会有碎片,那我们把内存一分为二,每次只用一半,如何?

划分:把内存分成大小相等的两块 A 和 B。只使用 A。
回收时:把 A 中存活的对象,一口气全部复制到 B 中。然后直接把 A 全部清空。
交换:交换 A 和 B 的角色,下次只使用 B。
-
优点:
-
绝对无碎片:对象是紧密复制到新空间的,排列整齐。
-
效率极高:特别适合“垃圾多、存活对象少”的场景。需要拷贝的只有那么几个幸存者。
-
-
致命缺点:内存利用率低,直接打折。只能使用总内存的一半,另一半总是闲置,对内存开销太大。
👉 小贴士:因为在新生代中,90%的对象都会“朝生夕灭”(活不过第一次回收),所以复制算法在新生代大放异彩。
4. 标记压缩法(Mark-Compact):老年代的“终极武器”
那如果在老年代(对象存活率高),用复制算法岂不是要把大量的对象搬来搬去,成本极高?于是,标记压缩法应运而生,它也被称为 Mark-Sweep-Compact(标记-清除-压缩)。

标记:和标记清除一样,先找出所有存活对象。
压缩(关键):不直接清除垃圾。而是将所有的存活对象往内存的一端移动,挤在一起。
清除:清理掉边界以外的所有内存空间。
-
优点:既解决了碎片化问题,又不需要像复制算法那样浪费一半的内存。
-
缺点:移动对象需要暂停应用线程(Stop-The-World),如果存活对象太多,移动成本较高。
三、 实战组合:分代算法(Generational Collection)
既然每种算法都有优缺点,聪明的JVM设计者提出了“分代假设”:“绝大多数对象都是朝生夕死的;熬过多次回收的对象,则很难死去。”
基于这个假设,JVM将堆内存划分为新生代和老年代,分别使用不同的算法。
📍 新生代(Young Generation) – 使用【复制算法】
-
特点:对象存活率极低。
-
内存分配:并不是简单的 1:1 分割。为了优化空间利用率,JVM 将新生代划分为:
-
Eden(伊甸园):占 80%。新对象诞生地。
-
Survivor 0 (From):占 10%。
-
Survivor 1 (To):占 10%。
-
-
回收过程(Minor GC):
-
回收时,Eden 和 From 区中的存活对象,会一股脑复制到 To 区。
-
如果对象特别大,或者 To 区满了,就直接晋升到老年代。
-
清理完 Eden 和 From 后,交换 From 和 To 的角色。
-
💡 为什么这样设计? 这样既保留了复制算法“无碎片、效率高”的优点,又将空间浪费从 50% 降低到了 10%(只浪费一个Survivor区)。
📍 老年代(Old Generation) – 使用【标记压缩法】
-
特点:存活率高,生命周期长。
-
回收过程(Major GC / Full GC):使用标记压缩法,效率更高,但同时产生的停顿时间也明显长于新生代回收。
📍 冷知识:卡表(Card Table) – 解决跨代引用性能瓶颈
老年代的对象有可能引用新生代的对象。如果在新生代回收时,要遍历整个老年代去检查有没有引用,那速度慢得不可想象。
JVM 使用了一个巧妙的位图数据结构——卡表:

-
工作原理:将老年代划分成许多 4KB 的小块。卡表中的一个 bit 就代表这块 4KB 区域。
-
0:表示这个 4KB 区域内的老年代对象,没有任何一个引用了新生代对象。
-
1:表示这个 4KB 区域内,至少有一个对象引用了新生代对象。
-
-
效果:在新生代 GC 时,只需查看卡表。如果卡表位是 0,直接跳过该区域。这大大减少了扫描对象的时间,加速了垃圾回收。
四、 现代趋势:分区算法(G1收集器的核心)
说完分代,我们再来看一种近年来非常火热的思想——分区算法(主要应用于 G1 垃圾收集器)。

-
痛点:分代回收中,一旦触发 Full GC(针对整个老年代/堆),如果内存巨大,停顿时间(STW)会非常久,严重影响业务。
-
解决办法:放弃物理上的新生代/老年代划分,把整个堆空间划分成若干大小相等的“小区间”(Region)。每一个小区间都可以独立使用、独立回收。
-
优势:由于区间很小,GC 时不需要回收整个堆,而是根据目标停顿时间,每次回收一部分(比如 20 个)区间。这就把原来几秒钟的大停顿,拆解成了几十毫秒的小停顿,避免了“全屋大扫除”带来的业务阻塞。
五、 技术总结与面试避坑指南
| 标记清除 | 标记+清除垃圾 | 简单、空间利用率高 | 产生内存碎片 | 老年代(CMS收集器) |
| 复制算法 | 存活对象复制到新空间 | 无碎片、速度快 | 空间浪费严重(需双倍空间) | 新生代(Eden/Survivor) |
| 标记压缩 | 标记+压缩存活对象 | 无碎片、空间利用率高 | 移动对象耗时,停顿时间长 | 老年代 |
| 分代算法 | 结合各算法优点 | 针对性强,整体效率最佳 | 需处理跨代引用(卡表) | 整堆(主流的宏观策略) |
| 分区算法 | 分小块独立回收 | 控制停顿时间精准 | 实现难度高 | G1/Garbage-First 收集器 |
网硕互联帮助中心

评论前必须登录!
注册