Java 垃圾回收(Garbage Collection, GC)是 JVM 运行时内存管理的核心机制之一,用于自动回收不再使用的对象,避免内存泄漏并提高开发效率。
一、 GC算法的历史与演进
Java GC算法的发展是一个持续优化的过程,主要经历了以下几个阶段:
- 早期(JDK 1.0 - 1.4): 这个阶段的GC相对简单,主要以串行(Serial)收集器为主,适用于内存较小、单核处理器的环境。
- 并发与并行(JDK 5 - 8): 随着多核处理器的普及和应用规模的增大,对GC的停顿时间(Stop-The-World, STW)要求越来越高。CMS和G1等并发和并行收集器应运而生,显著降低了GC对应用的影响。
- 低延迟与高吞吐(JDK 9+): 针对更大内存、更高并发的场景,以及对GC停顿时间极致追求的需求,ZGC和Shenandoah等更先进的收集器被引入,它们旨在实现毫秒级甚至亚毫秒级的GC停顿。
二、判断对象存活的方法
在垃圾回收器对堆内存回收前,需要判断对象是否存活。
- 引用计数算法: 给每个对象添加一个引用计数器,每当对象被引用, 对象的引用计数器就加1,当引用失效时,引用计数器就减1。 直到引用计数器为0,就代表对象不再被引用。
- 可达性算法: 通过GC ROOT的对象节点往下搜索,节点走过的路径被称为引用链。 如果一个对象不处于任何引用链,那么就可以判断此对象是不可达的。
三、什么是GC Root
GC Root(垃圾回收根)是 Java 垃圾回收(GC)算法中用于判断对象是否"存活"的起始点。

主要包括:
- 栈中的引用(局部变量)
- 静态变量
- 常量引用
- JNI 全局引用
- 线程、ClassLoader 等特殊引用
GC Roots 可达性分析过程:
- 从 GC Roots 出发。
- 沿着对象引用关系搜索。
- 标记所有可达对象为存活。
- 未被标记的对象即为垃圾。
GC Roots 是一组 JVM 默认认为"永远存活"的对象引用,即使两个对象互相引用,但如果从 GC Roots 无法到达它们,它们依然会被回收。
四、常见的GC算法
引用计数 (Reference Counting)
一种历史悠久且直观的算法
引用计数是一种非常简单且直接的垃圾回收算法。它的核心思想是:
- 为每个对象维护一个引用计数器。
- 当有一个新的引用指向该对象时,计数器加 1。
- 当一个引用失效(例如,引用被重新赋值为
null
,或者引用离开了作用域)时,计数器减 1。 - 当对象的引用计数器变为 0 时,说明没有其他对象再引用它,这个对象就可以被回收了。
优势:
- 引用计数器归零时,对象可以立即被回收,无需等待完整的 GC 周期。这使得它的停顿时间非常短。
- 更直观,容易理解。
劣势:
- 致命问题就是处理不了循环引用,需要维护一个计数器。
标记-清除 (Mark-Sweep)
最基础的 GC 算法

其过程分为两个阶段:
- 标记 (Mark) :GC 会从一组被称为 GC Roots 的对象(如活跃线程的栈变量、静态变量等)开始,遍历所有可达(即被引用)的对象,并给它们打上"存活"的标记。
- 清除 (Sweep):在标记阶段完成后,GC 会遍历整个堆内存。所有未被标记的对象都被认为是垃圾,其占用的内存将被回收。
优势:
- 算法逻辑相对直观,易于实现。
劣势:
- 碎片化严重,因为被回收的内存都是不连续的,尽管短时间回收了大部分空间,但如果突然需要分配一个大对象,而没有连续的空间可用,从而提前触发另一次 GC,甚至导致 OutOfMemoryError。
- 标记和清除都需要遍历整个堆,如果堆内存很大,效率会比较低。
复制 (Copying) 算法
为了解决标记-清除算法的内存碎片问题,复制算法应运而生

它将可用内存划分为大小相等的两块,通常被称为 "From 空间" 和 "To 空间"。每次只使用其中一块内存。
- 当 From 空间内存用完时,GC 会将所有存活的对象从 From 空间复制到 To 空间,并且这些对象在 To 空间中是紧凑排列的。
- 复制完成后,From 空间的所有内存可以直接被清空。
- 然后,From 空间和 To 空间的角色互换,下一次 GC 时使用新的 From 空间。
优势:
- 不会产生连续的内存碎片,速度也很高效。
劣势:
- 正如所描述的那样,需要划分两个相等的块,内存利用率直接减半,代价高昂。
- 如果存活对象很多,复制的开销就会很大,更不适用于存活对象多的区域(老年代)
复制算法一般用于新生代。 因为新生代的GC非常频繁,每次GC的对象较多,存活的对象较少。 所以采用复制算法效率更高,复制时只需要复制少量存活的对象。
标记-整理 (Mark-Compact)
标记-整理算法是对标记-清除算法的一种改进。

它结合了标记-清除和复制算法的优点:
- 标记 (Mark):与标记-清除算法相同,首先标记出所有可达对象。
- 整理 (Compact) :在标记完成后,GC 不会直接清除死亡对象,而是将所有存活的对象都向内存的一端移动,然后直接清理掉这一端边界以外的内存。
优势:
- 通过移动存活对象,消除内存碎片。
- 不像复制算法那样需要牺牲一半的内存空间。
劣势:
- 所谓慢工出细活,标记-整理的效率是比标记-清除要低的,因为对象移动会涉及到内存拷贝和引用地址的更新。
标记-整理算法和标记-清除算法一样通常被用于 Java 的老年代,因为老年代的对象存活率较高,不适合使用复制算法。
分代收集 (Generational Collection)
分代收集算法并不是指某一种具体的垃圾收集算法, 而是将复制,标记-清除,标记-整理等算法合理运用到堆区的不同空间。 比如新生代使用复制算法,老年代使用标记清除或标记整理算法。
现代的几乎所有的JVM都使用分代收集,毕竟每种算法都有优缺点, 结合它们的特点,对不同的环境采用不同的算法是非常明智的选择。
分代收集基于两个重要的经验法则:
- 弱代假设:绝大多数对象都是"朝生夕灭"的,在新生代中很快就会死亡。
- 强代假设:熬过越多次垃圾收集过程的对象就越难以死亡,它们倾向于长时间存活。
根据这两个假设,堆内存通常被划分为:
- 新生代 (Young Generation) :
- 主要存放新创建的对象。
- 根据弱代假设,这里对象的死亡率非常高。
- 通常采用复制算法 进行垃圾回收(称为 Minor GC),因为复制少数存活对象比清除大量死亡对象更高效。
- 新生代又被细分为一个 Eden 区 和两个 Survivor 区 (From Space 和 To Space)。
- 老年代 (Old Generation) :
- 存放经过多次 Minor GC 仍然存活的对象(即长期存活的对象)。
- 根据强代假设,这里对象的死亡率较低。
- 通常采用标记-整理算法 或标记-清除-整理结合 的算法进行垃圾回收(称为 Major GC 或 Full GC),以避免内存碎片,并适应高存活率的特点。
优势:
通过对不同代的特点使用不同的 GC 算法,大大优化了整体的垃圾回收性能。
五、一次GC的过程
对象优先在eden区被分配,当eden区内存不足时, JVM发起Minor GC。Minor GC的范围包括eden和From Survivor:
首先JVM会根据可达性算法标记出所有存活的对象。
如果存活的对象中,有的对象的年龄已经达到晋升阈值 (阈值是动态计算的,可以通过-XX:MaxTenuringThreshold设置最大年龄阈值), 那么将已经达到阈值的对象复制到老年代中。
如果To Survivor空间不足以存放剩余存活对象, 则直接将存活的对象提前复制到老年代。 如果老年代也没有足够的空间存放存活的对象, 那么将触发Full GC(GC整个堆,包括新生代和老年代)。
如果To Survivor可以存放存活的对象, 那么将对象复制到To Survivor空间,并清理eden和From Survivor。
此时From Survivor为空, 那么From Survivor就成为了下一次的To Survivor, 此时To Survivor存放着存活的对象,就成为了下一次的From Survivor。 这样From Survivor与To Survivor就是不断交替复制的使用。
老年代的空间比新生代的空间要大, 所以老年代的Major GC要比Minor GC耗时更长。 根据垃圾回收器的不同,老年代的GC算法也不同。
六、动态年龄阈值
动态年龄阈值是 JVM 为了防止 Survivor 区溢出,根据对象年龄分布动态调整晋升年龄的智能机制 ------ 不一定等到年龄 15,只要"某批对象快占满 Survivor",就提前晋升。
JVM并不要求对象年龄一定要达到 MaxTenuringThreshold 才会 晋升到老年代,晋升的年龄阈值是动态计算的。 如果在Survivor中,某个相同年龄阶段的所有对象大小的总和 大于Survivor区域的一半,则大于等于这个年龄的所有对象 可以直接进入老年代,无需等到MaxTenuringThreshold。
七、最后
算法名称 | 关键步骤 | 核心特点 | 典型应用场景 |
---|---|---|---|
标记-清除 | 1. 从GC Roots遍历标记存活对象 2. 直接回收未标记区域 | 产生内存碎片 | 早期JVM老年代 |
复制算法 | 1. 将存活对象从From区复制到To区 2. 清空整个From区 | 需要双倍内存空间 | 新生代(Survivor区) |
标记-整理 | 1. 标记存活对象 2. 将所有存活对象向内存一端移动 3. 清理边界外空间 | 移动对象开销大,无碎片 | 老年代 |
分代收集 | - 新生代:复制算法 - 老年代:标记-清除/标记-整理 | 针对不同生命周期对象优化 | 现代JVM默认方案 |
关键对比维度:
- 内存效率:复制算法 > 标记-整理 > 标记-清除
- 空间利用率:标记-整理(100%)> 标记-清除(有碎片)> 复制算法(50%)
- 时间复杂度:标记-清除 ≈ 复制算法 < 标记-整理(需对象移动)