垃圾回收
# 垃圾回收



Java 中垃圾回收的重点区域是?

从次数上讲:
- 频繁收集 Young 区
- 较少收集 Old 区
- 基本不动方法区
# 垃圾回收算法
# 垃圾标记阶段算法
在堆里存放着几乎所有的 java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为己经死亡的对象, GC 才会在执行垃圾回收时,释放掉其所占用的内存空间, 因此这个过程我们可以称为垃圾标记阶段。
那么在 JVM 中究竟是如何标记一个死亡对象呢?
简单来说,当一个对象已经不再被任何的存活对象继续引用时,就可以宣判为已经死亡。
# 引用计数算法
原理:对于一个对象 A,只要有任何一个对象引用了 A ,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,即表示对象 A 不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
- 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在 Java 的垃圾回收器中没有使用这类算法。

举例:
public class RefCountGC {
//这个成员属性唯一的作用就是占用一点内存
private byte[] bigSize = new byte[4 * 1024 * 1024];
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
//不加如下的操作,就没有垃圾回收
//这里发生GC,obj1和obj2能否被回收?
System.gc();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

如果就想用此算法,怎么解决循环引用?
手动解除:很好理解,就是在合适的时机,解除引用关系。 使用弱引用 weakref。
# 可达性分析算法
原理:其原理简单来说,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和 GC Roots 之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。
优点:实现简单,执行高效 ,有效的解决循环引用的问题,防止内存泄漏。

# GC Roots
在 Java 语言中, GC Roots 包括以下几类元素:
- 虚拟机栈中引用的对象
- 比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 方法区中类静态属性引用的对象
- 方法区中常量池中的引用
- 所有被同步锁 synchronized 持有的对象
- Java 虚拟机内部的引用。
- 基本数据类型对应的 Class 对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError),系统类加载器。
- 本地方法栈内 JNI (通常说的本地方法) 引用的对象
- 反映 java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等。
# 垃圾清除阶段算法
当成功区分出内存中存活对象和死亡对象后, GC 接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。目前在 JVM 中比较常见的三种垃圾收集算法是标记-清除算法( Mark-Sweep )、复制算法( Copying )、标记 - 压缩算法( Mark-Compact ) 。
# 标记 - 清除算法
标记:对存活的对象进行标记。 清除:清除没有标记的,也就是垃圾对象。(很多书、视频讲错了!说是标记的垃圾对象。这里要注意了!)

缺点:
- 效率比较低:递归与全堆对象遍历两次
- 在进行 GC 的时候,需要停止整个应用程序,导致用户体验差
- 这种方式清理出来的空闲内存是不连续的,产生内存碎片。
# 复制算法

优点:
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间的连续性,不会出现 “碎片” 问题。
缺点:
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 另外,对于 G1 这种分拆成为大量 region 的 GC,复制而不是移动,意味着 GC 需要维护 region 之间对象引用关系,这个开销也不小,不管是内存占用或者时间开销
应用场景: 在新生代,对常规应用的垃圾回收,一次通常可以回收 70%-99% 的内存空间。回收性价比很高。所以现在的商业虚拟机都是用这种收集算法回收新生代。
比如:IBM 公司的专门研究表明,新生代中 80% 的对象都是 “朝生夕死” 的。
# 标记 - 压缩算法
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以 JVM 的设计者需要在此基础之上进行改进。标记 - 压缩(Mark - Compact)算法由此诞生。

优点:(此算法消除了 “标记 - 清除” 和 “复制” 两个算法的弊端。)
- 消除了标记 / 清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM 只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点:
- 从效率上来说,标记 - 压缩算法要低于复制算法。
- 效率不高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
- 对于老年代每次都有大量对象存活的区域来说,极为负重。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。即:STW
# 分代收集算法
三种算法的对比:
| Mark-Sweep | Mark-Compact | Copying | |
|---|---|---|---|
| 速度 | 中等 | 最慢 | 最快 |
| 空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的 2 倍大小(不堆积碎片) |
| 移动对象 | 否 | 是 | 是 |
效率上来说,复制算法是当之无愧的老大,但是却浪费了太多内存。而为了尽量兼顾上面提到的三个指标,标记 - 整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记 - 清除多了一个整理内存的阶段。
目前几乎所有的 GC 都是采用分代收集(Generational Collecting)算法执行垃圾回收的。在 HotSpot 中,基于分代的概念, GC 所使用的内存回收算法必须结合年轻代和老年代各自的特点。
- 年轻代 (Young Gen):年轻代特点是区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
- 老年代 (Tenured Gen):老年代的特点是区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。

# 分区算法
分区算法:---G1 GC 使用的算法
分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。
每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。
一般来说,在相同条件下,堆空间越大,一次 GC 时所需要的时间就越长,有关 GC 产生的停顿也越长。为了更好地控制 GC 产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次 GC 所产生的停顿。




# 相关概念
# System.gc()
在默认情况下, 通过 System.gc () 或者 Runtime.getRuntime ().gc () 的调用,会显式触发 Full GC,同时对老年代和新生代进行回收。会有一些效果,但是系统是否进行垃圾回收依然不确定。
而一般情况下,垃圾回收应该是自动进行的,无须手动触发, 否则就太过于麻烦了。
# 内存泄漏与内存溢出
javadoc 中对 OutOfMemoryError 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 首先说没有空闲内存的情况:说明 Java 虚拟机的堆内存不够。原因有二:
- Java 虚拟机的堆内存设置不够。
- 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集(存在被引用)
内存泄漏(Memory Leak) 也称作 “存储渗漏”,用动态存储分配函数动态开辟的空间,在使用完毕后未被释放,结果导致一直占据该内存单元,一直持续到程序结束,即所谓内存泄漏。
举例: 1、单例模式 单例的生命周期和应用程序是一样长的,所以单例程序中,如果持有对外部对象的引用的话,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。
2、一些提供 close 的资源未关闭导致内存泄漏 数据库连接(dataSourse.getConnection ()),网络连接 (socket) 和 io 连接必须手动 close,否则是不能被回收的。
# STW
Stop-the-World ,简称 STW ,指的是 GC 事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为 STW 。
- STW 事件和采用哪款 GC 无关, 所有的 GC 都有这个事件。
- 哪怕是 G1 也不能完全避免 Stop-the-world 情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高, 尽可能地缩短了暂停时间。
- 被 STW 中断的应用程序线程会在完成 GC 之后恢复, 频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少 STW 的发生。
- 开发中不要用 System.gc (); 会 stop the world 的错误。
# 4 种引用
- 强引用:不回收
- 软引用:内存不足即回收
- 弱引用:发现即回收
- 虚引用:对象回收跟踪
# 垃圾回收的并行与并发
并发和并行,在谈论垃圾收集器的上下文语境中,它们可以解释如下:
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。 如 ParNew、Parallel Scavenge、Parallel Old;

串行(Serial)
- 相较于并行的概念,单线程执行。
- 如果内存不够,则程序暂停,启动 jvm 垃圾回收器进行垃圾回收。回收完,再启动程序的线程。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),垃圾回收线程在执行时不会停顿用户程序的运行。
用户程序在继续运行,而垃圾收集程序线程运行于另一个 CPU 上; 如:CMS、G1

# 安全点与安全区域
# 内存快照分析工具
# 垃圾回收器
# GC 分类
- 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。

- 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。
- 并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
- 独占式垃圾回收器( Stop the world) 一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。
- 按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。
- 压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。
- 非压缩式的垃圾回收器不进行这步操作。
- 按工作的内存区间,又可分为年轻代垃圾回收器和老年代垃圾回收器。
# GC 评估指标
- 吞吐量:程序的运行时间(程序的运行时间+内存回收的时间)。

- 垃圾收集开销:吞吐量的补数,垃圾收集器所占时间与总时间的比例。
- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
- 收集频率:相对于应用程序的执行,收集操作发生的频率。
- 内存占用: Java 堆区所占的内存大小。
- 快速: 一个对象从诞生到被回收所经历的时间。
吞吐量优先:单位时间内,STW 的时间最短:0.2 + 0.2 = 0.4 响应时间优先:尽可能让单次 STW 的时间最短:0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5
红色的三项共同构成一个 “不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。 这三项里,低延迟的重要性日益凸显。
# Serial GC:串行回收
Serial 收集器采用复制算法、串行回收和 “Stop-the-World” 机制的方式执行内存回收。

优势:简单而高效(与其他收集器的单线程比),对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
- 在 HotSpot 虚拟机中,使用
-XX: +UseSerialGC参数可以指定使用年轻代串行收集器和老年代串行收集器。- 等价于 新生代用 Serial GC,且老年代用 Serial Old GC
# ParNew GC:并行回收
如果说 Serial GC 是年轻代中的单线程垃圾收集器,那么 ParNew 收集器则是 Serial 收集器的多线程版本。
- Par 是 Parallel 的缩写,New:只能处理的是新生代
ParNew 收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别,因为 ParNew 收集器在年轻代中同样也是采用复制算法、“ Stop-the-World" 机制。

在程序中,开发人员可以通过选项 -XX:+UseParNewGC 手动指定使用 ParNew 收集器执行内存回收任务。它表示年轻代使用并行收集器。
# Parallel GC:吞吐量优先
HotSpot 的年轻代中除了拥有 ParNew 收集器是基于并行回收的以外, Parallel 收集器同样也采用了复制算法、并行回收和 “Stop the World” 机制。
那么 Parallel 收集器的出现是否多此一举?
和 ParNew 收集器不同,Parallel Scavenge 收集器的目标则是达到一个可控制的吞吐量(Throughput),它也被称为吞吐量优先的垃圾收集器。

-XX:+UseParallelGC手动指定年轻代使用 Parallel 并行收集器执行内存回收任务。-XX:+UseParallelOldGC手动指定年轻代和老年代都是使用并行回收收集器。
# CMS:低延迟
在 JDK 1.5 时期,HotSpot 推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器 ——CMS (Concurrent-Mark-Sweep) 收集器,这款收集器是 HotSpot 虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。
在 G1 出现之前,CMS 使用还是非常广泛的。

-XX :+UseConcMarkSweepGC:手动指定使用 CMS 收集器执行内存回收任务
# G1 GC:区域化分代式
既然我们已经有了前面几个强大的 GC,为什么还要发布 Garbage First(G1)GC?
原因就在于应用程序所应对的业务越来越庞大、复杂,用户越来越多,没有 GC 就不能保证应用程序正常进行,而经常造成 STW 的 GC 又跟不上实际的需求,所以才会不断地尝试对 GC 进行优化。G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一。
- G1(Garbage-First)是一款面向服务端应用的垃圾收集器,兼顾吞吐量和停顿时间的 GC 实现。
- 在 JDK1.7 版本正式启用,是 JDK 9 以后的默认 GC 选项,取代了 CMS 回收器。

# 各 GC 使用场景
- Serial 收集器:串行运行;作用于新生代;复制算法;响应速度优先;适用于单 CPU 环境下的 client 模式。
- ParNew 收集器:并行运行;作用于新生代;复制算法;响应速度优先;多 CPU 环境 Server 模式下与 CMS 配合使用。
- Parallel Scavenge 收集器:并行运行;作用于新生代;复制算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
- Serial Old 收集器:串行运行;作用于老年代;标记 - 整理算法;响应速度优先;单 CPU 环境下的 Client 模式。
- Parallel Old 收集器:并行运行;作用于老年代;标记 - 整理算法;吞吐量优先;适用于后台运算而不需要太多交互的场景。
- CMS 收集器:并发运行;作用于老年代;标记 - 清除算法;响应速度优先;适用于互联网或 B/S 业务。
- G1 收集器:并发运行;可作用于新生代或老年代;标记 - 整理算法 + 复制算法;响应速度优先;面向服务端应用。
# GC 的组合
由于维护和兼容性测试的成本,在 JDK 8 时将 Serial+CMS、ParNew+Serial Old 这两个组合声明为废弃(JEP 173),并在 JDK 9 中完全取消了这些组合的支持(JEP214)
JDK14 新特性:ParallelScavenge + SerialOld GC 的 GC 组合要被标记为 Deprecate 了。

# Epsilon GC
Epsilon GC (http://openjdk.java.net/jeps/318),只做内存分配,不做垃圾回收的 GC,对于运行完就退出的程序非常适合。称为无操作的垃圾收集器。

# Shenandoah GC
Shenandoah,无疑是众多 GC 中最孤独的一个。是第一款不由 Oracle 公司团队领导开发的 HotSpot 垃圾收集器。不可避免的收到官方的排挤。比如号称 OpenJDK 和 OracleJDK 没有区别的 Oracle 公司仍拒绝在 OracleJDK12 中支持 Shenandoah。
Red Hat 研发 Shenandoah 团队对外宣称,Shenandoah 垃圾回收器的暂停时间与堆大小无关,这意味着无论将堆设置为 200 MB 还是 200 GB, 99.9% 的目标都可以把垃圾收集的停顿时间限制在十毫秒以内。
| 收集器 | 运行时间 | 总停顿 | 最大停顿 | 平均停顿 |
|---|---|---|---|---|
| Shenandoah | 387.602s | 320ms | 89.79m5 | 53.01ms |
| Gl | 312.052s | 11.7s | 1.24s | 450.12ms |
| CMS | 285.264s | 12.78s | 4.39s | 852.26ms |
| Parallel Scavenge | 260.092s | 6.59s | 3.04s | 823.75ms |
# ZGC
令人震惊、革命性的 ZGC
ZGC 与 Shenandoah 目标高度相似,在尽可能对吞吐量影响不大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。
《深入理解 Java 虚拟机》一书中这样定义 ZGC:ZGC 收集器是一款基于 Region 内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记 - 压缩算法的,以低延迟为首要目标的一款垃圾收集器。


# 相关 VM 参数
-XX:PrintGCDetails:打印垃圾回收的详细信息-verbose:gc:打印垃圾回收的日志信息