垃圾收集

对象已死?

引用计数算法

1
2
3
很多教科书判断对象是否存活的算法是这样的:

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

优点

1
引用计数算法(Reference Counting)虽然占用了一些额外的内存空间来进行计数,但它的原理简单,判定效率也很高,在大多数情况下它都是一个不错的算法。

缺点

1
这个看似简单的算法有很多例外情况要考虑,必须要配合大量额外处理才能保证正确地工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。

可达性分析算法

1
2
3
4
当前主流的商用程序语言(JavaC#,上溯至前面提到的古老的Lisp)的内存管理子系统,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。

基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为
“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

image-20220116140237501

GC Roots对象

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等
  • 方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
  • 方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用
  • 本地方法栈中JNI(Native方法)引用的对象
  • Java虚拟机内部引用的对象,譬如基本数据类型对应的Class对象,常驻的异常对象,系统类加载器
  • 所有被同步锁(synchronized关键字)持有的对象
  • 反应Java虚拟机内部情况的对象,譬如JMXBean、JVMTI中注册的回调、本地代码缓存等
1

引用

  • 强引用,类似Object obj = new Object()这种引用关系,只要强引用关系存在,垃圾收集器就永远不会收掉被引用的对象
  • 软引用,有用,但非必须的对象。在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收
  • 弱引用,描述那些非必须对象,但是它的强度比软引用更弱一些,每次都会被垃圾收集器收集
  • 虚引用,最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知

生存还是死亡

1
2
3
4
5
6
7
即使在可达性分析算法中判定为不可达的对象,也不是“非死不可”的,这时候它们暂时还处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程:

如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,

随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。

假如对象没有覆盖finalize()方法(无药可救),或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

垃圾收集算法

1
2
3
从如何判定对象消亡的角度出发,垃圾收集算法可以划分为“引用计数式垃圾收集”(ReferenceCounting GC)和“追踪式垃圾收集”(Tracing GC)两大类,这两类也常被称作“直接垃圾收集”和“间接垃圾收集”。

由于引用计数式垃圾收集算法在本书讨论到的主流Java虚拟机中均未涉及,所以我们暂不把它作为正文主要内容来讲解,本节介绍的所有算法均属于追踪式垃圾收集的范畴。

注意

1
2
3
4
5
6
7
部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,其中又分为:

新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。

混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
1
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。

标记-清除

image-20220116151303548

优点

1
速度非常快

缺点

1
2
3
4
1.是执行效率不稳定,如果Java堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低

2.容易产生内存碎片,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
比如上图中有一块较大的对象想存放,但是存放不下

标记-整理

image-20220116151526306

使用场景

1
老年代

优点

1
没有内存碎片

缺点

1
添加了一步整理过程,需要移动对象,速度相对较慢

复制

image-20220116151753876

image-20220116151804412

image-20220116151814884

image-20220116151831573

使用场景

1
现在的商用Java虚拟机大多都优先采用了复制收集算法去回收新生代;但不适用于老年代,因为存活率高,会有较多的复制操作,效率将会降低

优点

1
不会产生内存碎片,速度也相对较快

缺点

1
占用双倍的内存空间

分代收集

Java堆内存

image-20220116153223569

新生代

1
使用Minor GC/Young GC,复制算法

老年代

1
使用Major GC/Old GC,标记-整理算法

经典垃圾收集器

STW

1
Stop The World”这个词语也许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。

Serial收集器

1
2
3
4
5
6
7
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。

是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

写到这里,笔者似乎已经把Serial收集器描述成一个最早出现,但目前已经老而无用,食之无味,弃之可惜的“鸡肋”了,但事实上,迄今为止,它依然是HotSpot虚拟机运行在客户端模式下的默认新生代收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的

在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代(仅仅是指新生代使用的内存,桌面应用甚少超过这个容量),垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

运行示意图

image-20220116154120409

Serial Old收集器

1
Serial OldSerial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。

运行示意图

image-20220116160108978

ParNew收集器

1
2
3
4
5
6
7
ParNew收集器实质上是Serial收集器的多线程并行版本

ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因是:

除了Serial收集器外,目前只有它能与CMS收集器配合工作。

自JDK 9开始,ParNew加CMS收集器的组合就不再是官方 推荐的服务端模式下的收集器解决方案了

运行示意图

image-20220116154549099

注意

1
2
3
4
5
6
并行和并发都是并发编程中的专业名词,在谈论垃圾收集器的上下文语境中,它们可以理解为:

并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程是处于等待状态。

并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。
由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于垃圾收集器线程占用了一部分系统资源,此时应用程序的处理的吞吐量将受到一定影响。

Parallel Scavenge收集器

1
2
Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器……
Parallel Scavenge的诸多特性从表面上看和ParNew非常相似

特点

1
达到一个可控制的吞吐量,所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值

image-20220116155443385

参数设置

1
2
3
4
5
控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis

直接设置吞吐量大小的-XX:GCTimeRatio

自适应的调节策略-XX:+UseAdaptiveSizePolicy

Parallel Old收集器

1
Parallel OldParallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

运行示意图

image-20220116160223688

CMS收集器

1
2
3
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现

目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

特点

1
以获取最短回收停顿时间为目标的收集器

运行过程

  • 初始标记
1
会触发STW,仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
  • 并发标记
1
GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
  • 重新标记
1
会触发STW,是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(详见3.4.6节中关于增量更新的讲解),这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短
  • 并发清除
1
清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

运行示意图

image-20220116161932917

缺点

1
2
3
4
5
1. CMS收集器对处理器资源非常敏感。

2. CMS收集器无法处理“浮动垃圾”(Floating Garbage)

3. CMS是一款基于“标记-清除”算法实现的收集器,缺点非常明显,一是不稳定,二是有垃圾碎片

Garbage First收集器

1
2
3
Garbage First(简称G1)收集器是垃圾收集器技术发展历史上的里程碑式的成果,它开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。

G1是一款主要面向服务端应用的垃圾收集器。

适用场景

1
2
3
同时注重吞吐量和低延迟,默认暂停目标 200ms
超大堆内存,会将堆划分为多个大小相等的Region
整体上是标记+整理算法,两个区域之间是复制算法

理解

1
2
3
首先要有一个思想上的改变,在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。

而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而 是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。

TAMS

1
G1为每一个Region设计了两个名为TAMSTop at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。

SATB

1
原始快照

运行过程

  • 初始标记
1
会触发STW,仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。
  • 并发标记
1
GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记
1
会触发STW,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  • 筛选回收
1
会触发STW,负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。

运行示意图

image-20220116164950814

image-20220116170208884

Young Collection

image-20220116170446228 image-20220116170502125 image-20220116170513803

Young Collection + Concurrent Mark

image-20220116170629991

Mixed Collection

image-20220116170818400

相关参数

1
2
3
-XX: +UseG1GC
-xx: G1HeapRegionSize = size
-xx: MaxGCPauseMillis = time

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!