目录
前言
上一篇文章讲到对象内存的回收,本篇文章将会进一步深入记录进行垃圾对象内存回收的三种算法和不同的实现。
一、垃圾收集算法
传统的JVM通常根据对象的存活周期的不同而将堆区内存分为新生代和老年代,那么在进行垃圾回收的时候,也要考虑到两部分内存中对象的特点,分别实现不同的垃圾收集算法,以达到高效回收垃圾的目的。这种设计思维便是分代收集理论。下面三种算法便是进行垃圾回收的基础算法。
1、标记-复制算法
类似于操作系统的内存管理和磁盘管理,JVM会将堆区内存划分为若干个大小相同的块,一个对象可以占据一个或多个内存块。
标记复制算法会将内存划分为两个包含相同数量内存块的分区。其中一个分区用于存放存活的对象,为使用区,另一个分区是保留区,在进行垃圾回收的时候使用。
如上图,在回收前,算法会将垃圾对象标记为灰色。在标记完成后,算法会将存活的对象复制到保留区,并将使用区的对象全部清除,这样原来的使用区就变为保留区,原来的保留区变为使用区。
由于标记-复制算法涉及到复制操作,很显然该算法适用于每次只进行少量复制的情景,而新生代每次在进行垃圾回收时都会清除掉大量的对象,因此可以使用该算法。该算法有个缺点便是一半的内存不能够使用。
2、标记-清除算法
标记-清除算法分为标记和清除阶段。标记便是找出需要清除的对象,清除便是清除掉被标记的垃圾对象,如下图:
该算法解决了标记-复制算法的问题,但又产生了新问题,那便是在进行多次垃圾回收之后,会产生大量的内存碎片。
3、标记-整理算法
标记整理算法综合解决了上面两种算法的缺点,其中标记阶段还是使用标记-复制算法和标记-清除算法的方式,不过在清除之前加入了整理,也就是将标记的存活对象整理到内存的一端,最后的清除便是将存活对象边界之外的内存全部清空。如下图:
标记-整理算法既解决了内存只能使用一半的问题,也解决了产生内存碎片的问题。
二、垃圾收集器
垃圾收集器是垃圾回收算法的具体实现。
1、Serial收集器
Serial收集器是单线程收集器,分为新生代和老年代两个版本,JVM参数分别为-XX:+UseSerialGC
和-XX:+UseSerialOldGC
,在JDK1.5之前和Parallel收集器搭配使用。新生代使用标记-复制算法,老年代使用标记-整理算法。
什么是单线程收集器呢?我们知道,JVM在运行程序时,往往会开出多个应用程序线程,并分别为这些线程分配单独的内存空间。单线程收集器便是使用一个线程来专门进行垃圾回收,这明显会产生一个问题,那就是在垃圾回收的过程中需要暂停应用程序线程,也就是STW(Stop The World)
,这可能会严重影响到用户的体验。
2、Parallel收集器
针对Serial收集器的问题,JVM设计者们设计了一种多线程并发的收集器,适用于堆内存小于4G的机器。与Serial收集器一样,新生代使用标记-复制算法,老年代使用标记-整理算法。该收集器在进行垃圾回收时,开出多个垃圾回收线程,提升回收速度。但该收集器在垃圾回收的过程中也需要STW
,若回收的垃圾特别多,仍然会影响用户的体验。新生代和老年代收集器的JVM参数分别为-XX:+UseParallelGC
和-XX:+UseParallelOldGC
。
3、ParNew收集器
该收集器原理和Parallel收集器很类似,不同点是其能够和CMS收集器搭配使用,设置参数为-XX:+UseParNewGC
。
4、CMS收集器
4.1 工作原理
CMS(Concurrent Mark Sweep)收集器是一种真正意义上的并发多线程收集器,它考虑到用户的体验,以获取最短回收停顿时间为目标,多用于堆内存在4~8G的机器。该收集器是标记-清除算法的一个实现,一般和ParNew收集器搭配使用,即新生代使用ParNew收集器,老年代使用CMS收集器,JVM参数分别为-XX:+UseParNewGC
和-XX:+UseConcMarkSweepGC
。
下图是CMS收集器的工作流程:
可以看到,CMS收集器回收过程比较复杂,分为以下步骤:
- 初始标记:会
STW
,开启一个CMS线程对GC Roots
进行标记,被标记的这些对象是非垃圾对象,此时不进行可达性分析,因此标记速度很快。对象标记使用的是三色标记法 - 并发标记:让CMS线程和应用程序线程并发运行,对初始标记时标记的那些
GC Roots
进行可达性分析。此过程比较复杂,消耗的时间比较长,因此和应用程序并发运行能够提升用户体验。对象标记使用的是三色标记法 - 重新标记:由于在并发标记阶段CMS线程和应用程序线程并发运行,被标记对象的状态可能会发生改变,因此需要
STW
来对那些发生改变的被标记对象进行标记修正。重新标记使用的是基于三色标记的增量更新法。 - 并发清除:让应用程序线程继续执行,同时开启CMS线程来并发清除未被标记的垃圾对象。对于在这个阶段产生的新垃圾对象,根据增量更新法,它们会在下一次的GC被回收。
- 并发重置:重置本次GC过程中的标记数据。
从以上流程可以看出,CMS收集器将需要花费大量时间的初始标记过程和应用程序并发执行,具有低停顿的特点,很注重用户的体验。
但该收集器也有缺点,一是在并发标记和并发清除阶段产生的新垃圾对象,只能够在下一次GC时被回收。二是其实现的标记-清除算法所具有的产生内存碎片的缺点,不过可以设置JVM参数-XX:+UseCMSCompactAtFullCollection
,在每次GC后都对内存进行一次整理。三是在并发标记和并发清理阶段可能会再次触发新的GC,甚至可能老年代在GC过程中加入了新对象导致放不下,出现concurrent mode failure
,从而触发Full GC
,此时老年代收集器会退化成Serial Old
单线程收集器,大大降低效率。
4.2 三色标记法和增量更新法
由于CMS收集器的并发标记阶段和并发清理阶段是和应用程序线程并发运行的,因此在这两个阶段中难免会有被标记对象的状态发生改变的情况,由此造成多标或漏标。
那么CMS垃圾收集器是如何对这两种情况进行处理的呢?JVM设计实现了增量更新法。
三色标记法是可达性分析算法的底层实现,该算法将从GC Roots
出发进行可达性分析而访问到的对象标记为三种状态,可以理解为三种颜色:黑、灰和白。
- 黑色对象:已经被垃圾收集器访问过,且是存活的,它的所有引用也被扫描过,如有其它对象引用该对象,垃圾收集器不会再重新扫描该对象。
- 灰色对象:已经被垃圾收集器访问过,但该对象的所有引用中至少有一个没有被扫描。
- 白色对象:没有被垃圾收集器访问过。在GC之前,所有对象都是白色的,在可达性分析之后,如果对象仍然是白色的,说明该对象不可达。
对于多标的情况,假如在可达性分析过程中,一个方法中的某个局部变量是GC Root
,其引用的对象被标记为非垃圾对象,也就是黑色对象,而接下来由于方法结束而导致局部变量被销毁,其引用的对象按理应该成为垃圾对象,但后续的可达性分析不会重新扫描黑色对象,由此该对象变成了浮动垃圾,直接放到下一次GC来清理。
对于漏标的情况,假如在可达性分析过程中,一个对象被标记为黑色,它的一个A
类型的引用变量值为null
,A
类型的一个对象a
还没被扫描过,为白色,而后来该黑色对象的引用变量指向了a
,垃圾收集器又不会重新扫描黑色对象,按理白色对象a
会在并发清理阶段被当作垃圾清理掉,从而产生严重bug。
针对这种问题,JVM实现了写屏障+增量更新法(Incremental Update)。当黑色对象对其它白色对象产生新的引用关系时,在引用赋值之前,插入写屏障的代码(说白了就是一个函数),将该引用关系记录下来(引用对象和被引用对象)。并发标记阶段结束后,在重新标记阶段,垃圾收集器会以记录过的黑色对象为根,重新进行可达性分析(可以认为黑色对象变成了灰色对象),从而将白色对象a
重新标记为灰色对象或黑色对象,a
就能避免被当作垃圾对象清除掉。
用图来演示漏标的情况:
在初始标记阶段,A
被标记为GC Root
,在并发标记阶段,由于扫描到了其引用的对象B
,且此时A
到D
的引用为null
,那么此时A
就会被标记为黑色。由于对象B
有指向C
和D
的引用,但垃圾收集器扫描完了C
,还没扫描到D
,因此B
被标记为灰色,D
仍为白色。
垃圾收集器还没扫描到D
,B
指向D
的引用没了,那么当前GC Root
及其引用的对象都扫描完了。
在后面的并发标记阶段,A
又引用了D
,而垃圾收集器在重新标记阶段不会再扫描A
及其引用,因此就造成了漏标,若不处理,D
会被当成垃圾对象。对于漏标,CMS垃圾收集器会使用写屏障+增量更新法来解决。
5、G1收集器
5.1 工作原理
G1(Garbage-First)从JDK1.7开始引入,于JDK1.9成为默认垃圾收集器,是一款面向服务器的垃圾收集器,适用于有多颗处理器和大内存(大于8G)的机器,具备垃圾收集停顿时间短和高吞吐量的特征,可以认为,G1垃圾收集器是专门为大负载线上系统而设计的,JVM参数为-XX:+UseG1GC
。
不同于之前的垃圾收集器,G1垃圾收集器在物理上消除了新生代和老年代的内存划分,但在逻辑上仍然划分为新生代和老年代,还加入了大对象专属的Humongous
区,因此G1垃圾收集器在逻辑上将堆内存划分为Eden
区、Survivor
区、Old
区和Humongous
区。G1垃圾收集器将堆内存平均划分为若干块,每个块被称为一个Region
,最多有2048块,Region
的大小可以通过参数-XX:G1HeapRegionSize
设置,每一块内存属于四区中的一区或者未被使用。示例图如下:
新生代的默认初始堆内存占比是5%,可以通过参数-XX:G1NewSizePercent
调整。在程序运行过程中,JVM会根据情况动态提升新生代的占比,但默认不会超过60%,可以通过参数-XX:G1MaxNewSizePercent
调整。默认情况下,Eden
区和Survivor
区的比例仍然是8:1:1。
在逻辑上,G1垃圾收集器对新生代对象转移至老年代的判断和之前的垃圾收集器一样,而区别是对短期存活的大对象的处理。G1垃圾收集器专门为短期存活的大对象设置了Humongous
区,判断规则是对象的大小超过了一个Region
大小的50%,也就是说,如果需要分配内存的大对象大小超过了一个Region
大小的50%,会被直接分配至Humongous
区。大对象不可能一直待在Humongous
区,其要么在Full GC
中被回收掉,要么分代年龄达到15而被移入老年代。
G1垃圾收集器的回收流程和CMS收集器极其相似,如下图:
- 初始标记:和CMS一样。
- 并发标记:和CMS类似。
- 重新标记:和CMS类似。
- 筛选回收:在G1版本需要
STW
,Shenandoah实现了并发收集。该阶段有一个很重要的参数-XX:MaxGCPauseMillis
,表示用户期望的最大GC停顿时间。G1垃圾收集器首先会对每个Region
按照回收价值(能回收多少空间)和成本(耗时)进行排序,再根据设置的停顿时间来选出最佳的回收方案。比如,设置停顿时间为100ms,那么G1收集器会计算出100ms能够回收的最大Region
数量和最大内存空间,以此方案进行垃圾回收。G1收集器回收空间采用的是复制算法,且以Region
为单位,直接将一个Region
存活的对象复制到另外一个Region
中。相对于CMS,G1回收垃圾所产生的内存碎片很少。
G1垃圾收集器的流程和CMS大同小异,但对象标记的底层实现有所不同,G1实现的是三色标记法+原始快照
5.2 原始快照
我在上面记录了CMS收集器的对象标记实现的是三色标记+写屏障+增量更新,而G1收集器的对象标记实现的是三色标记+写屏障+原始快照(Snapshot At The Beginning, SATB)。
那么什么是原始快照呢?
当灰色对象要删除对白色对象的引用时,如这张图:
会通过写屏障将该引用给记录下来,在重新标记阶段,再以这些引用的灰色对象为根,将扫描到的白色对象全部标记为黑色对象,留到下一次GC来处理。很显然,这些被标记为黑色的对象,最后可能会变成浮动垃圾,也就是出现了多标,直接留到下一次GC处理即可。
5.3 G1垃圾收集分类
- Young GC:也就是minor GC,不过在
Eden
区满后,G1收集器不会马上做Young GC,而是计算最优地回收现在的Eden
区需要多少时间,将该时间和停顿时间做比较,若小于停顿时间,则对Eden
区进行扩容处理。不断重复之前的操作,直至计算出来的回收时间接近停顿时间,此时才会进行Young GC。 - Mixed GC:当
Old
区内存使用率达到JVM参数-XX:InitiatingHeapOccupancyPercent
设定的阈值时,G1收集器会对新生代、大对象区和部分老年代进行回收,回收方式为G1收集器工作流程中的筛选回收,老年代回收的部分同时由停顿时间和优先回收顺序决定。 - Full GC:若采用复制算法进行回收时,没有多余的
Region
来存放存活对象,G1收集器就会退化成单线程收集器,发生Full GC。
5.4 CMS和G1的异同
对比CMS收集器和G1收集器:
- 两者都真正意义上做到了并发,而后者能够充分发挥多核CPU的优势,实现并行处理。
- 前者是老年代的收集器,后者是新生代、老年代和大对象区的混合收集器。
- 两者在逻辑上都为分代收集,但后者在物理上取消了分代隔阂,且专门为大对象引入了
Humongous
区,降低了Old
区的压力。 - 前者是基于标记-清理-可选整理算法的实现,后者宏观上是标记-整理算法的实现,实际筛选回收阶段是复制算法的实现。
- 两者的对象标记底层实现都是三色标记,对于漏标的处理,前者实现的是写屏障+增量更新,后者实现的是写屏障+原始快照。
- CMS和G1都以减少停顿时间而提升用户体验为目标,而后者实现了可控的低停顿时间。
为什么CMS和G1对于漏标的处理方式不一样呢?
我认为这得根据两个算法的效率和两者的物理内存划分来理解。其一,增量更新算法在重新标记阶段会继续深度扫描发生增量引用的对象,而原始快照算法在重新标记阶段只是将被记录的引用对象标记为黑色,留到下一次GC处理,不会继续深入扫描,显然原始快照的效率更高。其二,CMS收集器将堆内存划分为新生代和老年代,老年代就只有那么一块内存区域,使用增量更新方法效率也不低,而G1收集器将堆内存划分为许多Region
,每一个逻辑分区都占有若干个Region
,不同的对象也分布在不同的Region
,这种情况下想要对对象进行深入扫描显然代价比较大,因此更适合使用原始快照。
6、ZGC收集器
占坑。
总结
本篇文章记录了对象内存回收的相关算法和不同的垃圾回收器实现。根据用途的不同,可以将这些垃圾收集器画成下图:
其中实现箭头表示相连的两个收集器可搭配使用,虚线箭头表示由于老年代内存空间不够,现用垃圾收集器暂时退化成Serial Old收集器来进行Full GC。