一、垃圾回收器总览
垃圾收集可以划分为几个阶段。。
第一阶段:单线程收集时代(Serial和Serial Old)
第二阶段:多线程收集时代(Parallel Scanvenge 和Parallel Old)
第三阶段:并发收集时代(ParNew和CMS)
第四阶段:智能并发收集时代(G1)
下面的图一方面介绍了有哪些垃圾收集器,另外一方面也描述了每个垃圾收集器是负责哪个分代(新生代、老年的)的垃圾收集,还有一部分信息是告诉我们每个新生代的垃圾收集器可以与哪些老年代的搜集配合工作。
新生代收集器:Serial、ParNew、Parallel Scavenge;
老年代收集器:Serial Old、CMS、Parallel Old;
通用收集器: G1;
常用组合:Serial+Serial Old, Parallel Scavenge+Parallel Old,ParNew+CMS,G1(不需要组合其他收集器)。
二、Serial 和Serial Old收集器
Serial 系列的垃圾收集器是JVM的第一款收集器,因为当时的硬件环境配置都不高,这个时候JVM的内存都是几十M,CPU也都是单核的,当时也没有现在这样的高的并发应用场景。所以限于当时的硬件资源和应用场景,所以它的设计思路就是简单高效、消耗资源最少、使用单线程收集。
Serial 垃圾收集流程
Serial 垃圾收集过程很简单,根据下图一眼就能明白,Serial会开启一个线程进行垃圾收集,在收集的整个过程都会暂停用户线程(Stop the Word),直到垃圾收集完毕,如果把垃圾收集的过程当作打扫房间卫生,那么Serial 的收集过程就是在你收集房间的时候,你首先会让房间里的人都出去,然后你再安心打扫房间,直到你打扫完毕了才能让外面的人进来,这样就不用担心你一边打扫房间一边还有人在房间里扔垃圾了。
注意:说到“暂停用户线程”,这里也是各种垃圾收集器的一个区分指标,后面的有些垃圾收集器收集的某些阶段是不需要暂停用户线程的。
收集器特点
收集区域: Serial (新生代),Serial Old(老年代)。
使用算法: Serial (标记复制法),Serial Old(标记整理法)。
搜集方式: 单线程收集。
优势: 内存资源占用少、单核CPU环境最佳选项。
劣势: 整个搜集过程需要停顿用户线程。多核CPU、内存富足的环境,资源优势无法利用起来。
三、Parallel Scavenge 和 Parallel Old收集器
随着硬件资源的升级,JVM的内存空间从原来的几十M可以扩展到几百M到甚至几G了,CPU也从单核走向了多核时代,此时使用Serial收集器还无法发挥出多核CPU的优势,因为内存空间变得更大了使用单线程收集的Serial收集的时间也变长了,所以就衍生了Parallel 系列的收集器,Parallel 核心是利用了多核CPU资源的优势,进行垃圾收集时可以多个线程同时进行收集,从而提升整个垃圾收集的性能 。
Parallel Scavenge工作流程
Parallel Scavenge 和Parallel Old的工作机制一样,这里以Parallel Scavenge为例,Parallel Old在收集过程中会开启多个线程一起收集,整个过程都会暂停用户线程,直到整个垃圾收集过程结束。和之前的Serial垃圾收集器一对比,同样进行垃圾收集前都是先叫其他人都离开房间,但是不同的是serial只有一个人打扫房间,而这里却是有多个人一起打扫房间,所以从这一点看Parallel 系列的收集器要比之前的效率高上很多。
收集器特点
收集区域: Parallel Scavenge (新生代),Parallel Old(老年代)。
使用算法: Parallel Scavenge (标记复制法),Parallel Old(标记整理法)。
搜集方式: 多线程。
优势: 多线程收集,CPU多核环境下效率要比serial高。
劣势: 整个搜集过程需要停顿用户线程。
四、ParNew 收集器
ParNew和Parallel Scavenge 垃圾收集器并没有太大的区别,能让ParNew出名的一个核心因素是因为它是唯一一个能与CMS配合一起使用新生代收集器,而因为CMS的优秀所以让ParNew 也出了名,这个就有点傍到大款的感觉。
ParNew收集器流程
ParNew收集流程和Parallel Scavenge一样 ,同样是先停止应用程序线程,再进行多线程同时收集,整个收集过程都会暂停用户线程(Stop the Word),直到垃圾收集完毕。
ParNew的特点
收集区域: 新生代。
使用算法: 标记复制法。
搜集方式: 多线程。
搭配收集器: CMS。
优势: 多线程收集,CPU多核环境下效率要比serial高,新生代唯一一个能与CMS配合的收集器。
劣势: 整个搜集过程需要停顿用户线程。
五、CMS收集器
随着硬件技术的发展,我们的服务器可用的内存越来越大,JVM各个区域可分配的空间越来越大,这样的好处就是相对于以前内存可以创建更多的对象了,然后垃圾收集总体次数也会减少,不过与此同时也伴随着另外一个问题出现了,因为内存空间的增大,JVM每次进行垃圾收集的时间就变得越来越长了,这个时候垃圾的收集经常会耗费几秒甚至几十秒,而之前的垃圾收集器进行收集的整个过程都是需要“停止用户线程”的,试想一下如果你点击系统的一个按钮十几秒都没有响应,我估计你是无法忍受的,所以这个时候就需要一种能不停止用户线程的垃圾收集器,让垃圾收集的同时也能处理“用户线程”的工作,这也就是CMS诞生的初衷。
CMS工作流程
为了尽量减少用户线程的停顿时间,CMS采用了一种全新的策略使得在垃圾回收过程中的某些阶段用户线程和垃圾回收线程可以一起工作,这样就避免了因为长时间的垃圾回收而使用户线程一直处于等待之中。
整个过程就像我们打扫房间的时候可以让大家留在房间里工作,等我把房间的其他地方都打扫完,只剩大家工作的那部分区域的垃圾,这个时候再让大家到房间外面去,我再把房间里那些剩下的地方清理干净就行了,这样做的好处就是大家的工作时间变长了,在房间外等待的时间变短了。
CMS 也是按这个逻辑把整个垃圾收集的过程分成四个阶段,分别是初始标记、并发标记、重新标记、并发清理四个阶段,然后CMS会根据每个阶段不同的特性来决定是否停顿用户线程。
阶段一:初始标记
初始标记的目的是先把所有GC Root直接引用的对象进行标记,因为需要避免在标记GC Root的过程还有程序在继续产生GC Root对象,所以这个过程是需要需要停止用户线程 ,因为这个过程只会标记GC Root的直接引用,并不会对整个GC Root的引用进行遍历,所以这个过程速度也是所有阶段中最快的。
阶段二:并发标记
并发标记阶段的工作就是把阶段一标记好的GC Root对象进行深度的遍历,找到所有与GC Root关联的对象并进行标记,这个过程中是采用多线程的方式进行遍历标记,对整个JVM 的GC Root进行遍历的过程是垃圾收集过程中最耗时的一步,CMS为了考虑尽量不停顿用户线程,所以这个阶段是不停止用户线程的,也就是说这个阶段JVM会分配一些资源给用户线程执行任务,通过这样的方式减少用户线程的停顿时间。
阶段三:重新标记
因为在阶段二的时候用户线程同时也在运行,这个过程中又会产生新的垃圾,所以重新标记阶段主要任务是把上一个阶段中产生的新垃圾进行标记( 使用多线程标记),很显然这个过程是对上一个阶段用户线程运行遗留的垃圾进行标记,所以数量 是非常少执行时间也是最短的,当然为了避免这个过程再次产生新的垃圾,所以重新标记的过程是会停顿用户线程的。
阶段四:并发清理
并发清理阶段是对那些被标记为可回收的对象进行清理,在一般情况下并发清理阶段是使用的标记清除法,因为这个过程不会牵扯到对象的地址变更,所以CMS在并发清理阶段是不需要停止用户线程的。也正因为并发清理阶段用户线程也可以同时运行,所以在用户线程运行的过程中自然也会产生新的垃圾,这也就是导致CMS收集器会产生“浮动垃圾”的原因。
当然,在一种情况下并发清理阶段CMS也会停顿用户线程,这就和我们之前说过的CMS选用的垃圾回收算法有关系,因为一般情况下使用的都是标记清除法,但是标记清除法的弊端就是在于会产生空间碎片,所以当空间碎片到达了一定程度时,此时CMS会使用标记整理法解决空间碎片的问题,不过因为标记整理法会将对象的位置进行挪动并更新对象的引用的指向地址,那么这个过程中用户线程同时运行的话会产生并发问题,所以当CMS进行碎片整理的时候必须得停止用户线程。
CMS的特点
收集区域: 老年代。
使用算法: 标记清除法+标记整理法。
搜集方式: 多线程。
搭配收集器: ParNew。
优势: 多线程收集,收集过程不停止用户线程,所以用户请求停顿时间短。
CMS遗留的问题
CMS收集器开辟了一条垃圾收集的新思路,不过这么好的垃圾收集器却一直没有被Hospot虚拟机纳入到默认的垃圾收集器,到Jdk8使用的默认收集器都还是 Parallel scavenge 和 Parallel old,这其中非常重要的原因就是CMS遗留了几个比较头疼的问题。
1、浮动垃圾
在并发清理阶段因为需垃圾收集线程是和用户线程同时执行任务的,这个时候用户线程运行时产生的垃圾是无法在当前阶段进行回收的,所以这段时间用户线程产生的新垃圾只能遗留到下一次收集,这些在垃圾收集过程中新产生的垃圾我们称为浮动垃圾。
3、空间碎片整理造成卡顿
CMS在平常情况下会使用标记清除法进行回收,只有在老年代的空间碎片达到一定程度,这个时候就会使用标记整理法对内存的空间碎片进行整理,因为标记整理的过程需要移动对象的位置,所以这个过程只能Stop the word,这个时候内存越大那么这个收集时间就越长,造成这种卡顿现象。
2、可能导致系统长时间的假死。
因为在并发清除阶段会有新的对象产生,在有担保机制的情况下,当新生代垃圾清理的时候存活的对象大多,导致Survior区无法容纳全部的对象,这时就会触发担保机制,这里存活的对象里面会有一部分会直接进入老年代,所以在每次GC的时候老年代需要预留一部分内存出来,所以通常CMS 在老年代占用了大概百分之七八十的时候就进行FullGC。
不过这段时间的产生对象的总体大小是未知的,如果新生代存活的对象非常多,这些担保的对象转移到老年代的时候可能导致老年代预留的空间也不足以容纳,那么此时CMS不得不进行一次Stop the word 的Full GC ,因为此时堆空间已经完全占满,这个时候已经无法使用并发的清理方式进行收集了,所以此时只能停止用户线程来专心进行垃圾收集,而这时候老年代收集器不得不从CMS切换成Serial old垃圾收集器来进行垃圾收集 。
至于这里为什么要使用单线程的Serial old,而不选择多线程的Parallel Old,那是因为CMS的新生代收集器是ParNew,而ParNew只能与CMS和Serial Old配合),所以这也是个无奈的选择。而切换成Serial old来进行垃圾收集的时候就有问题了,Serial old收集器是单线程的,它只适用于内存大小在几十到上百M的大小,而往往我们现在的内存大小都是几G到几十G,所以这种情况下整个垃圾收集的时间可能会特别特别长,有时候可能达到几个小时甚至好几天的都有可能。
六、G1收集器
CMS开创了垃圾收集器的一个新时代,它实现了垃圾收集和用户线程同时执行,达到垃圾收集的过程不停止用户线程的目标,这个思路作为后面的收集器提供了一个很好的典范。时代向前优化不止,除了需要解决了CMS遗留了的几个问题外,硬件资源的升级换代,可用的内存资源越来越多一直是促进垃圾收集器发展的一个核心驱动力,可使用的内存资源变多对于软件来说这当然是个好事,不过对于垃圾收集器来说就变得越来越麻烦了,随着发展我们发现传统垃圾收集器的收集方式已经不适用于这种大内存的垃圾收集了。
不管是Serial系列、Parallel系列、CMS系列,它们都是基于把内存进行物理分区的形式把JVM内存分成老年代、新生代、永久代或MetaSpace,这种分区模式下进行垃圾收集时必须对某个区域进行整体性的收集(比如整个新生代、整个老年代收集或者整个堆), 原来的内存空间都不是很大,一般就是几G到几十G,但现在的硬件资源发展可用的内存达到几百G甚至上T的程度,那么JVM中的某一个分代区域就可能会有几十上百G的大小,那么如果这时候采用传统模式下的物理分区的收集的话,每次垃圾扫描内存区域变大了、那么需要的清理时间自然就会变得更加长了;换做打扫卫生来说,原来你只需要打扫几个小办公室就行了,但是随着公司业务发展整栋楼是都是你公司了,这个时候你需要打扫公司卫生的时间无疑也会变得特别长。
所以问题出现了,那么自然就有人会来解决的,G1就是在这种环境下诞生的,G1首先吸取了CMS优良的思路,还是使用并发收集的模式,但是更重要的是G1摒弃了原来的物理分区,而是把整个内存分成若干个大小的Region区域,然后由不同的Region在逻辑上来组合成各个分代,这样做的好处是G1进行垃圾回收的时候就可以用Region作为单位来进行更细粒度的回收了,每次回收可以只针对某一个或多个Region来进行回收。
Region(局部收集)
G1最核心的分区基本单位Region ,G1没有像之前一样把堆内存划分为固定连续的几块区域,而是完全舍弃了进行内存上的物理分区,而是把堆内存拆分成了大小为1M-32M的Region块,然后以Region为单位自由的组合成新生代、老年代、Eden区、survior区、大对象区(Humonggous Region),随着垃圾回收和对象分配每个Region也不会一直固定属于哪个分代,我们可以认为Region可以随时扮演任何一个分代区域的内存。
Collect Set(智能收集)
在G1里面会维护一个Collect Set集合,这个里面记录了待回收的Region块信息同时也包括了每个Region块可回收的大小空间,也正是因为有了这个有了这个CSet信息G1在进行垃圾收集的时候,就可以根据用户设定的可接受停顿时间来进行分析,在设定的时间范围内收集哪些区域最划算而择优先收集的区域,这样不仅每次可以优先收集垃圾最多的Region,还可以根据用户的设定之间来计算收集哪些Region达到用户所期望的垃圾收集时间,通过CSet让G1的垃圾回收性价比非常高,并且可以通过这个实现可预测的停顿时间要求,让垃圾回收变得智能化,当然用户设定的时间也不能脱离实际,官方建议是在100ms-300ms之间。
G1工作流程
G1的回收流程和CMS逻辑大致相同,分别进行初始标记、并发标记、重新标记、筛选清除,区别在最后一个阶段G1不会直接进行清除,而是会根据设置的停顿时间进行智能的筛选和局部的回收。
阶段一:初始标记
初始标记额目的是先把所有GC Root直接引用的对象进行标记,因为需要避免在标记GC Root的过程还有程序在继续产生GC Root对象,所以这个过程是需要停止用户线程 ,因为这个过程并不会对整个GC Root的引用进行遍历,所以这个过程速度是非常快的。
阶段二:并发标记
并发标记阶段的工作就是把阶第一段标记好的GC Root对象进行深度的遍历,找到所有与GC Root关联的对象并进行标记,这个过程中是采用多线程的方式进行遍历标记,对整个JVM 的GC Root进行遍历的过程是垃圾收集过程中最耗时的一步,为了尽量不停顿用户线程,所以这个阶段GC线程会和用户线程同时运行,通过这样的方式减少用户线程的停顿时间。
阶段三:最终标记
因为在上个阶段用户线程同时也在运行,用户线程运行的过程中又会产生新的垃圾,所以重新标记阶段主要任务是把上一个阶段中产生的新垃圾进行标记( 使用多线程标记),很显然这个过程是对上一个阶段用户线程运行遗留的垃圾进行标记,所以数量是非常少执行时间也是非常短的,当然为了避免这个过程再次产生新的垃圾,所以重新标记的过程是会停顿用户线程的。
阶段四:筛选回收
把存活的对象复制到空闲Region区域,再根据Collect Set记录的可回收Region信息进行筛选,计算Region回收成本,根据用户设定的停顿时间值制定回收计划,根据回收计划筛选合适的Region区域进行回收。
回收算法:从局部来说G1是使用的标记复制法,把存活对象从一个Region复制到另外的Region,但从整个堆来说G1的逻辑又相当于是标记整理法,每次垃圾收集时会把存活的对象整理到其他对应区域的Region里,再把原来的Region标记为可回收区域记录到CSet里,所以G1的每一次回收都是一次整理过程,所以也就不会产生空间碎片问题。
G1的特点
收集区域: 整个堆内存。
使用算法: 标记复制法
搜集方式: 多线程。
搭配收集器: 无需其他收集器搭配。
优势: 停顿时间可控,吞吐量高,可根据具体场景选择吞吐量有限还是停顿时间有限,不需要额外的收集器搭配。
劣势: 因为需要维护的额外信息比较多,所以需要的内存空间也要大,6G以上的内存才能考虑使用G1收集器。
总结
从现在往回看,我们会发现每个垃圾收集器都是一个时代的产物。
第一阶段:在单核CPU,内存资源稀缺的时代使用的是Serial和Serial Old收集器,对于单核CPU,内存只有几十M的场景Serial的效率是非常高的。
第二阶段:进入多核CPU时代后出现了Parallel Scavenge和Parallel Old收集器,利用多线程并行收集极大的提高了垃圾收集的效率,所以在多核CPU场景,内存在几百M到几G的场景Parallel Scavenge和Parallel Old是适用的。
第三阶段:随着内存的变大,垃圾收集的过程时间变得越来越长了,BS系统的发展也逐渐开始重视用户体验了,所以就出现了CMS以减少用户线程停顿时间为目的的收集器,CMS通过并发收集减少了用户线程的停顿时间,在多核CPU,并且内存空间几G到几十G的空间、并且注重用户体验的CMS垃圾收集器是适用的。
第四阶段:CMS遗留了一些比较致命的问题,所以就有了G1,G1不再对内存进行物理上的分代,而只是进行逻辑上的分区,通过各种机制让垃圾收集变得更智能和可控了,多核CPU,并且内存在10G到上百G的场景G1比较适合。
标签:收集,标记,收集器,线程,垃圾,JVM,CMS From: https://blog.51cto.com/u_16209090/7051112