JVM垃圾收集器和垃圾收集算法

260 阅读11分钟

JVM垃圾收集器

Serial收集器

Serial收集器是最基本,发展最悠久的收集器,在JDK1.3.1之前是虚拟机新生代垃圾回收的唯一选择。这个收集器是一个单线程的。它的单线程的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成收集工作,最重要的是,它进行垃圾收集时,其他工作线程会暂停,直到收集结束。这项工作由虚拟机在后台自动发起和执行的,在用户不可见的情况下将所有工作线程全部停掉,这对于很多应用程序来说是不可容忍的。我们可以设想一下,我们的计算机在运行1个小时就要停止5分钟的时候,这是什么情况?对于这种设计,虚拟机设计人员表示的也是非常委屈,因为不可能边收集,这边还要不断的产生垃圾对象,这样是清理不完的。所以从1.3一直到现在,虚拟机的开发团队一直为减少因垃圾回收而产生的线程停顿所努力着,所出现的虚拟机越来越优秀,但直到现在,依然没有完全消除。

讲到这里,貌似Serial收集器已经是"食之无味弃之可惜"了,但实际上,它依然是虚拟机在Client模式下,新生代默认的垃圾收集器。它有相对于其他垃圾收集器的优势,比如由于没有线程之间切换的开销,专心做垃圾收集自然能够收获最高的线程利用效率。在用户桌面应用背景下,一般分配给虚拟机的内存不会太大,收集几十兆或者一两百兆的新生代对象,停顿时间完全可以控制在几十毫秒到一百毫秒之间,这个是可以接受的,只要不是频繁发生。因此,Serial收集器在Client模式下,对于新生代来说依然是一个很好的选择。

Serial收集器

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾回收之外,其余可控参数,收集算法,停止工作线程,对象分配原则,回收策略等与Serial收集器完全一致。

除了多线程实现垃圾收集之外,其他没有什么太多创新之处,但是它确实Server模式下的新生代的首选的虚拟机收集器。其中一个重要的原因就是除了Serial收集器外,只有它能与CMS配合使用。在JDK1.5时期,HotSpot推出了一款在强交互应用划时代的收集器CMS,这款收集器是HotSpot第一款真正意义上的并发收集器,第一次实现了垃圾回收与工作线程同时工作的可能性,换而言之,你可以边污染,边收集。

不过CMS作为老年代的收集器,却无法与1.4中发布的最新的新生代垃圾收集器配合使用,反之只能使用Serial或者Parnew中的一个。ParNew收集器可以使用-XX:+UseParNewGC强行指定它,或者使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器。

ParNew收集器在单CPU环境下绝对不会有比Serial收集器更好的效果,甚至优于存在线程交互开销,该收集器在通过超线程技术实现的两个CPU的环境下都不能保证百分之百超越Serial收集器。当然,随着CPU数量的增加,对于GC时系统的有效资源利用还是很有好处的。在CPU非常多的情况下,可以使用-XX:ParallelGCThreads来限制垃圾回收线程的数量。

ParNew收集器

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,采用复制算法,又是并行的多线程垃圾收集器。它的关注点与其它收集器的关注点不一样,CMS等收集器的关注点在于缩短垃圾回收时用户线程停止的时间,而Parallel Scavenge收集器则是达到一个可控制的吞吐量,所谓吞吐量就是CPU运行用户线程的时间与CPU运行总时间的比值,即 吞吐量 = (用户线程工作时间)/(用户线程工作时间 + 垃圾回收时间),比如虚拟机总共运行100分钟,垃圾收集消耗1分钟,则吞吐量为99%。停顿时间越短越适合与用户交互的程序,良好的响应速度能提高用户体验,但是高吞吐量则可以高效率的利用CPU的时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的程序。

有两个参数控制吞吐量,分别为最大垃圾收集时间: -XX:MaxGCPauseMills, 直接设置吞吐量的大小: -XX:GCTimeRatio

-XX:+UseAdaptiveSizePolicy

自适应策略也是Parallel Scavenge收集器区别去Parnew收集器的重要一点

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法,这个收集器的主要目的也是在与给Client模式下使用。如果在Server模式下,还有两种用途,一种是在jdk5以前的版本中配合Parallel Scavenge收集器使用,另一种用途作为CMS的备用方案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法,这个收集器在jdk6中才开始使用的,在此之前Parallel Scavenge收集器一直处于比较尴尬的阶段,原因是,如果新生代采用了Parallel Scavenge收集器,那么老年代除了Serial Old之外,别无选择,由于老年代Serial在服务端的拖累,使得使用了Parallel Scavenge收集器也未必能达到吞吐量最大化的效果,由于单线程的老年代无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至不如Parallel Scavenge收集器 + CMS。

直到Parallel Old收集器出现后,"吞吐量优先收集器"终于有了名副其实的组合,在注重吞吐量优先和CPU资源敏感的场合,可以采用Parallel Scavenge收集器 + Parallel Old收集器。

Parallel Old收集器

CMS收集器

CMS收集器是一种以获取最短停顿时间为目标的收集器。从名字(Concurrent Mark Sweep)上就可以看出,采用的标记-清除算法,它的过程分为4个步骤:

只有初始标记和重新标记需要暂停用户线程。

(1)初始标记 --- 仅仅关联GC Roots能直接关联到的对象,速度很快;

(2)并发标记 --- 进行GC Roots Tracing的过程;

(3)重新标记 --- 为了修正并发标记期间,因用户程序运作而导致标记产生变动的那一部分对象的标记记录;

(4)并发清除

CMS收集器

由于整个过程中耗时最长的并发标记和并发清除过程收集器都能与用户线程一起工作,所以总的来说,CMS的内存回收过程与用户线程一起并发执行的

CMS收集器的三大缺点:

(1)CMS收集器对CPU资源非常敏感

(2)无法处理浮动垃圾

(3)因为基于标记清除算法,所以会有大量的垃圾碎片产生 -XX:+UseCMSCompactAtFullCollection

G1收集器

首先,G1的设计原则就是简单可行的性能调优

-XX:+UseG1GC -Xmx32g -XX:MaxGCPauseMillis=200

其中-XX:+UseG1GC为开启G1垃圾收集器,-Xmx32g 设计堆内存的最大内存为32G,-XX:MaxGCPauseMillis=200设置GC的最大暂停时间为200ms。如果我们需要调优,在内存大小一定的情况下,我们只需要修改最大暂停时间即可。

(1)内存分配

(2)Young垃圾回收

(3)Mix垃圾回收

G1收集器

常见设置参数:

G1收集器

JVM垃圾收集算法

垃圾收集算法有标记-清除算法、复制算法、标记真理算法、分代收集算法四种,下面我们详细介绍。

标记-清除算法:

最基础的收集算法是标记-清除算法,如同它的名字一样,分为标记和清除两个阶段。第一步标记出所要回收的对象,在标记完成后统一回收所有被标记的对象。如何标记已经在上面说过了,之所以说它是最基本的垃圾收集算法,原因在于其他的算法也是基于这种思路并对其不足做以改进得到的。

主要问题有两个:

第一个是效率问题,标记和清除的效率都不高。

标记清除算法

第二个是空间分配问题,标记清除后会产生大量的不连续的内存空间,空间碎片太多可能会导致以后程序在运行过程中需要给较大对象分配空间时,无法找到足够的内存空间,而不得不提前进行一次垃圾收集动作。如图所示,会产生大量的垃圾碎片,导致空间的利用率不高。

复制算法

为了解决效率问题,一种称为复制的收集算法出现了,它将可用内存分为大小相等的两块,每次只使用其中的一块,当这一块内存区域用完了,就将还存活的对象复制到另一块内存中,然后再把已使用的空间一次性清理掉,这样每次都是对半个区域进行回收,内存分配时也就不用考虑碎片等问题了,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

只是这种做法将原来的内存缩小为一半,代价太高了。

现在的商用虚拟机都采用这种方法来回收新生代,IBM专门研究表明,新生代中的对象98%都是"朝生夕死"的,所以并不需要按照1:1划分内存区域,而是将内存分为一块较大的区域给Eden和两块较小的区域给Survivor, 当回收时,将Eden和Survivor区中还存活的对象一次性复制到另一块Survivor区,然后将Eden和Survivor区进行一次性清理。Hotspot区默认的Eden和Survivor的比例为8:1,也就是说新生代的可用内存为90%,只有10%的内存会被划分为保留内存。当然,大多数情况下是98%,但我们不能保证每次回收的存活对象都小于10%,当Survivor区不够用时,需要依赖其他区域进行分配担保。如果另外一块Survivor区已经不够用了,对象可通过内存担保机制直接进入到老年代。

复制算法

标记整理算法

复制算法在存活对象比例比较高的情况下要进行较多的复制操作,效率将会变低,更关键的是,如果不想浪费50%的区域,则需要额外的空间进行分配担保,以应对内存中100%对象都存活的极端情况,所以老年代一般不选用这种算法。

根据老年代的特点,有人提出了另一种标记-整理算法,标记过程与标记-清除算法一致,但后续步骤不是对可回收对象直接进行清理,而是让所有存活对象都向一端移动,然后直接清理掉边界外的对象。示意图如下:

标记整理算法

分代收集算法

当前商用的垃圾收集器都采用的是分代垃圾回收,这种算法没有什么新的思想,只是根据对象的存活周期将内存分为几块,一般是将java堆分为新生代和老年代,这样就可以根据各个代的对象特点选用最适当的回收算法。在新生代,每次垃圾回收都有大量的对象死去,只有少量存活,这样就适合采用复制算法。只需要付出少量的对象复制成本就可以完成垃圾回收,而老年代因为存活率高,没有其他内存进行分配担保,就必须使用标记-清理或者标记-整理进行回收。

  1. 分代分为年轻代和老年代,年轻代里头又分为 Eden区和Survivor区,通常默认的比例为8:1:1, 每次只保留10%的空间用作预留区域,然后将90%的空间可以用作新生对象。

  2. 每一次垃圾回收之后,存活的对象年龄对应+1,当经历15次还依然存活的对象,我们让它直接进入到老年代;

  3. 另外一种进入到老年代的方式是内存担保机制,也就是当新生代的空间不够的时候,对象直接进入到老年代;

  4. 新生代的垃圾回收叫Minor GC,老年代的叫Full GC。

参考文章:

jvm垃圾收集器有哪些?深度讲解

垃圾收集算法有哪些?图文详细介绍