判断对象是否存活
引用计数法
给对象添加一个引用计数器,当有一个新的引用指向这个对象,就在计数器加一,引用失效减一。计数器为 0 时,认为该对象不会被再使用。
缺点:无法解决对象循环引用,如果两个对象互相引用,并且这两个对象都已不再使用。那么它们永远不会被回收。
可达性分析算法
使用 GC Roots 判断对象是否可达,通过一系列的”GC Roots” 对象作为起始结点,从这些结点到对象间的路径叫做引用链,当一个对象到 GC Roots 没有任何引用链,判定为可回收的对象
引用
- 引用划分:强引用、软引用、弱引用、虚引用
垃圾收集算法
标记-清除法
标记-清除法分为标记和清除两个阶段:首先在堆中标记这个对象是否应该被回收,标记完成后统一回收被标记的对象。
缺点:
- 标记-清除这两个过程的效率不高
- 空间问题:清除后会产生许多不连续的内存碎片,导致后续分配大对象时,仍旧无法找到空间,再次触发 GC
复制算法
将内存划分为相等的两块,当这块使用完,则把存活的对象复制到另一块。然后清理使用过的空间。
缺点:将内存缩小为原来的一半
标记-整理算法
首先标记出需要回收的对象,再将存活的对象都向一端移动,然后直接清理其余内存。
分代收集算法
根据对象的存活周期,把内存划分为几块,在不同的区域使用不同的回收算法。
一般是分为新生代和老年代,新生代会有大量的对象死亡,可以使用复制算法,老年代对象存活率较高,可以使用标记-清除,或者标记-整理算法。
HotSpot 的算法实现
安全点
程序执行时并非在所有地方都能暂停下来执行 GC ,必须在安全点。如何在 GC 发生时,让所有线程都跑到最近的安全点:
- 抢先式中断:可以理解为线程被动中断,在 GC 发生时,首先中断所有线程,如果有线程没有到达安全点,则恢复线程,使它跑到安全点
- 主动式中断:线程主动中断,不直接对线程操作,而是设置一个中断标志,线程主动轮询这个标志,一旦发现需要中断,则自动中断挂起
安全区域
指的是在一段代码内,引用关系不会发生变化,在这段区域内的任何地方都可以发生中断。
垃圾收集器
Serial 收集器
单线程收集器,不能与用户线程同时运行,必须暂停其他所有线程,直到收集结束。
优点:
- 简单高效,没有线程交互的开销。
- 在用户的应用场景中,分配给虚拟机的内存一般不会很大,因此需要收集的内存不多,通常停顿时间较短。
因此,仍然是 client 模式下默认的新生代收集器
ParNew 收集器
是 Serial 收集器的多线程版本(多个 GC 线程),但在单 CPU 的环境下,因为线程交互的开销,可能比 Serial 性能更差,它默认开启的收集线程数与 CPU 数量相同。
Parallel Scavenge 收集器
该收集器的关注点在于,达到一个可控制的吞吐量
吞吐量 = 用户代码运行时间/(用户代码运行时间+垃圾收集时间)
Serial Old 收集器
Serial 的老年代版本,主要给 client 模式下的虚拟机使用。
Parallel Old 收集器
Parallel Scavenge 的老年代版本,在注重吞吐量以及 CPU 资源敏感的场合,可以优先考虑 Parallel Scavenge + Parallel Old
CMS 收集器
以获取最短 GC 停顿时间为目标的收集器,它将垃圾收集分成了几个步骤,而其中耗时最长的步骤都可以与用户线程一起工作,因此降低了 GC 停顿时间。主要优点是:并发收集、低停顿
缺点:
- 无法处理“浮动垃圾”,即并发运行的用户线程产生的垃圾。
- 使用标记-清除算法产生的空间碎片
步骤:
- 初始标记
- 并发标记
- 重新标记
- 并发清理
G1 收集器
G1 是面向服务端应用的垃圾收集器,特点如下:
- 并行与并发:缩短停顿时间,达到并发
- 分代收集:采用不同的方式收集不同对象区域
- 空间整合:收集后会规整内存
- 可预测的停顿:化整为零,避免全堆扫描,优先回收价值最大的区域
步骤:
header 1 | header 2 |
---|---|
row 1 col 1 | row 1 col 2 |
row 2 col 1 | row 2 col 2 |
- 初始标记
- 并发标记
- 最终标记
- 筛选回收