一、基础的GC算法
第一步,记录所有存活对象
- 在垃圾收集中有一个叫标记(Marking)的过程专门干这件事。
(1)标记可达对象(Marking Reachable Objects)

首先指定特定对象为GC根节点(Garbage Collection Roots)
1)当前正在执行的方法的局部变量和输入法
2)活动线程(Active threads)
3)内存中所有类的静态字段(static field)
4)JNI引用
其次,GC遍历内存中整体的对象关系图,从GC根元素开始扫描,到直接引用,以及其他对象(通过对象的属性域)。所有访问到的对象都被标记(marked)为存活对象。
备注:
在标记阶段,需要暂停所有应用线程,以遍历所有对象的引用关系。因为不暂停没法跟踪一直变化的引用关系。这种叫Stop The World pause(全线停顿)。
(2)引用计数(Reference Counting)(不建议使用)

依据被引用的次数来确定对象是否存活,引用次数为0的则会标识为垃圾。
但这种算法有个弊端,就是容易被循环引用(detached cycle)给搞死。任何作用域中都没有引用这些对象,但由于循环引用,导致引用计数一直大于零。

3)三色标记算法
三色标记算法将每个节点归纳分类为三类:白色节点、灰色节点和黑色节点。采用深度优先搜索的策略。
| 颜色定义 | 状态转换 |
| 白色 | 初始状态,表示对象未被访问,可能是垃圾。 |
| 灰色 | 对象被访问,但其引用的子对象尚未检查。 |
| 黑色 | 对象及其子对象均被检查,确认为存活。 |
3-1)算法流程
3-1-1)初始化:所有对象标记为白色,根对象(如全局变量、栈变量)置灰并入队。
3-1-2)标记阶段:
- 1. 从队列中取出灰色对象,遍历其子对象。
- 2. 若子对象为白色,则置灰并入队。
- 3. 处理完成后,当前对象置黑。
3-1-3)回收阶段:标记完成后,白色对象视为不可达,被回收。
3-2)并发修改问题与解决方案
问题场景:黑色对象新增对白色对象的引用(用户程序并发修改),若此时无灰色对象引用该白色对象,会导致误回收(“悬挂引用”)。
解决方案:
写屏障技术:
1. 增量更新(Incremental Update):当黑色对象插入新引用时,将该引用目标(白色对象)直接标记为灰色,确保后续扫描。
2. 原始快照(Snapshot At The Beginning):记录修改前的引用关系,标记过程基于旧快照进行,保证一致性。
3-3)实际应用与优化
1. 并发垃圾回收器:如CMS、G1等采用三色标记,结合写屏障处理并发问题。
2. 并行标记优化:多线程并行处理灰色队列,需同步机制避免竞争。
3. 阶段协调:通过颜色状态划分标记阶段,确保逻辑清晰,适合分步或并发执行。
3-4)示例说明
3-4-1)初始状态:对象A(白)引用B(白)、C(白),根引用A。
3-4-2)标记过程:
1. A置灰入队。
2. 处理A时,B、C置灰入队,A置黑。
3. 处理B、C,若无引用则置黑,队列空。
3-4-3)并发修改:若处理A时新增对D(白)的引用,写屏障触发将D置灰,确保后续扫描。
第二步,删除不可达对象。
- 在不同算法中删除不可达对象略有不同,但总体可分为三类:清除(sweeping)、整理(compacting)和 复制(copying)。
(1)清除(sweep)
Mark and Sweep(标记清除)算法的概念:直接忽略所有垃圾。所有不可达对象占用的内存空间,都被认为是空闲的,因此可以用来分配新对象。

这种算法需要使用空闲表(free-list),来记录所有的空闲区域,以及每个区域的大小。维护空闲表增加了对象分配时的开销。此外还有一个缺点,明明有很多空闲内存,却可能没有一个区域的大小能够存放需要分配的对象,从而导致分配失败
(2)整理(Compact)
Mark-Sweep-Compact(标记-清除整理算法),将所有被标记的对象(存活的对象),迁移到内存空间的起始处,消除了标记-清除算法的缺点。

相应的缺点就是GC的暂停时间会增加,因为需要将所有对象复制到另一个地方,然后修改指向这些对象的引用。
有点也很明显,碎片整理后,分配新对象就很简单。
使用这种算法,内存剩余的容量一直是清除的,不会再导致内存碎片问题。
(3)复制(Copy)
Mark and Copy(标记-复制算法),将所有存活的对象移动到另一个内存空间。

优点是标记和复制可以同时进行。
缺点是需要一个额外的内存空间,来存放所有的存活对象。
二、使用中的GC算法

主要有上述黑体的四种组合方式:
(1)年轻代和年老代的串行GC(Serial GC)
(2)年轻代和年老代的并行GC(Parallel GC)
(3)年轻代的并行GC(Prallel New)+ 老年代的CMS(Concurrent Mark and Sweep)
(4)G1,负责回收年轻代和年老代
(1)年轻代和年老代的串行GC(Serial GC)
- Serial GC对年轻代使用mark-copy(标记-复制)算法,对年老代使用mark-sweep-compact(标记-清除-整理)算法,两者都是单线程的垃圾收集器,不能进行并行处理。
两者都会触发全线暂停(STW),停止所有的应用线程。
这种GC算法不能充分利用多核CPU,不管多少CPU内核,JVM在垃圾收集时都只能使用单个核心。
(2)年轻代和年老代的并行GC(Parallel GC)
- 并行垃圾收集器这一组合,在年轻代使用mark-copy(标记-复制)算法,在年老代使用mark-sweep-compact(标记-清除-整理)算法。
年轻代和年老代的垃圾回收都会触发STW事件,暂停所有的应用程序来执行垃圾收集。
两者在执行标记和复制/整理阶段时都使用多个线程,因此得名“(Parallel)”。通过并行执行,使得GC时间大幅减少。
(3)年轻代的并行GC(Parallel New)+年老代的CMS(Concurrent Mark and Sweep)
- 对年轻代采用并行STW方式的mark-copy(标记-复制)算法,对年老代主要使用并发mark-sweep(标记-清除)算法。
CMS的设计目的是避免在年老代垃圾收集时出现长时间的卡顿。
主要通过两种手段来达成此目标:
1)不对年老代进行整理,而是使用空闲列表(free-list)来管理内存空间的回收。
2)在mark-and-sweep(标记-清除)阶段的大部分工作和应用线程一起并发执行。
也就是说,在这些阶段并没有明显的应用线程暂停。
备注:CMS原理在下面补充知识点里有说明。
(4)G1,负责回收年轻代和年老代
- Garbage First(垃圾优先算法),将STW停顿的时间和分布变成可预期以及可配置的。G1是一款软实时垃圾收集器,也就是说可以为其设置某项特定的性能指标。
- 如:任意1秒暂停暂停时间不得超过5毫秒。
为了达成这项指标,G1有一些独特的实现。
首先,堆不再分为连续的年轻代和年老代空间,而是划分为多个(通常是2048个)可以存放对象的小堆区(smaller heap regions)。
每个小堆区都可能是Eden区,Survivor区或者Old区,在逻辑上,所有Eden区和Survivor区合起来就是年轻代,所有的Old区拼在一起那就是老年代。

这样的划分使得GC不必每次都去收集整个堆空间,而是以增量的方式来处理:每次只处理一部分小堆区,称为此次的回收集(Collection set)。每次暂停都会去收集所有年轻代的小堆区,但可能只包含一部分年老代小堆区。

G1的另一项创新,是在并发阶段估算每个小堆区存活对象的总数。用来构建回收集(Collection set)的原则是:垃圾最多的小堆区会被优先收集。这也就是G1名称的由来:garbage-first。
备注:
1)除了GC还有其他场景用安全点吗
安全点是指程序执行过程中的一些特定位置,在这些位置上,所有线程的状态都是已知的,JVM可以在这些位置进行某些需要暂停所有Java线程的操作。除了垃圾回收外,以下是一些使用安全点的场景:
1-1)代码除错和诊断工具:当使用调试器或者一些性能分析工具(如VisualVM、JProfiler等)时,为了获取准确的堆栈信息或者其他运行时数据,JVM可能需要暂停所有的Java线程来确保收集的数据一致性。
1-2)类卸载:在某些情况下,如果一个类不再被使用且其加载器也可以被回收,JVM会在安全点执行类卸载操作。这涉及到清理该类的所有静态变量和释放相关资源。
1-3)偏向锁撤销:在Java的同步机制中,偏向锁是一种优化技术,旨在减少无竞争情况下的锁开销。但在检测到锁竞争时,JVM需要在安全点撤销偏向锁并转换为轻量级锁或重量级锁。
1-4)线程转储:当生成线程转储(Thread Dump)以分析死锁或性能瓶颈时,也需要在安全点暂停线程,以便获取准确的线程状态信息。
2)为啥初始标记和重新标记需要stw
2-1)初始标记(Initial Mark):
在初始标记阶段,JVM会标记所有直接从GC根对象(如栈上的局部变量、活动线程、静态字段等)可到达的对象。
这个过程需要暂停所有的Java应用线程(即发生STW),因为如果允许应用线程继续运行,那么对象图可能会发生变化。
比如新的对象被分配或者对象引用关系发生变化,这将导致标记的信息不准确,可能错过一些存活的对象或错误地标记已死亡的对象。
2-2)重新标记(Remark):
重新标记阶段旨在修正并发标记期间由于应用线程继续修改对象图而造成的任何潜在错误。
尽管一些垃圾收集器(如G1收集器)尝试通过并发标记的方式减少停顿时间,但在并发标记阶段,应用线程仍然可能对堆进行修改。
为了确保最终的标记结果是正确的,重新标记阶段也需要一个短暂的STW暂停来处理这些变化,并且检查那些在并发标记阶段中发生变化的对象,以确保没有遗漏任何存活的对象。
更多java基础总结(适合于java基础学习、java面试常规题):
总结篇(9)---字符串及基本类 (1)字符串及基本类之基本数据类型
总结篇(10)---字符串及基本类 (2)字符串及基本类之java中公共方法及操作
总结篇(12)---字符串及基本类 (4)Integer对象
总结篇(14)---JVM(java虚拟机) (1)JVM虚拟机概括
总结篇(15)---JVM(java虚拟机) (2)类加载器
总结篇(16)---JVM(java虚拟机) (3)运行时数据区
总结篇(17)---JVM(java虚拟机) (4)垃圾回收
总结篇(18)---JVM(java虚拟机) (5)垃圾回收算法
总结篇(19)---JVM(java虚拟机) (6)JVM调优
总结篇(24)---Java线程及其相关(2)多线程及其问题
总结篇(25)---Java线程及其相关(3)线程池及其问题
总结篇(26)---Java线程及其相关(4)ThreadLocal
总结篇(27)---Java并发及锁(1)Synchronized
总结篇(31)---JUC工具类(1)CountDownLatch
本文深入解析了Java中的垃圾回收(GC)算法,包括基础的GC算法如标记-清除、标记-整理、复制等,以及实际使用的GC算法组合,如串行GC、并行GC、CMS和G1等。详细阐述了每种算法的工作原理、优势和不足。
5万+

被折叠的 条评论
为什么被折叠?



