《深入理解Java虚拟机系列三》--- 7+2种垃圾收集器(通俗易懂)

Posted 小样5411

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《深入理解Java虚拟机系列三》--- 7+2种垃圾收集器(通俗易懂)相关的知识,希望对你有一定的参考价值。

前言

本文是深入理解Java虚拟机第三章垃圾收集器的内容,这里也做一个通俗详细的解释,主要讲解7个经典垃圾收集器和2个最新发展的垃圾收集器

上一节:《深入理解Java虚拟机系列二》— 垃圾回收算法

一、7种经典垃圾收集器

先看下面这幅图,来了解下哪7个,不用记,后面会讲,看下就行
注意:经典收集器称为为经典,还有一层含义,就是技术成熟、稳定,其中CMS与G1尤为重要

Young generation:新生代
Tenured generation:老年代
图中上半部分垃圾收集器表示工作在新生代,而下半部分垃圾收集器表示工作在老年代,即新生代三个,老年代三个,中间夹个G1收集器

注意:连线表示,两个收集器可以搭配使用,一个工作在新生代,一个工作在老年代,收集器不一定最新的就最好,当然最新的肯定解决了以往收集器的一些痛点,没有哪个收集器是万能的,只有按照指定场景选择最适合的收集器进行搭配,下面就开始讲解。

1.1 Serial收集器

Serial收集器是最基础,历史最悠久的收集器,这个收集器是一个“单线程”工作的收集器,这里单线程除了指只能使用一个处理器,还另外强调它在垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。也就是单线程意思是只能它自己工作,别的都要停下来等它做完。这是十分影响体验的,想想你电脑如果停止响应几分钟,你基本会强制关机重启。这可以打一个比喻:“比如你妈妈在打扫房间,你肯定老老实实的坐在一旁,难道你妈妈一边打扫,然后你还一边制造垃圾吗,那房间怎么打扫的完?”。我们可以看到下图,GC线程来回收垃圾时,其他用户线程都要停止,就只剩GC线程工作,图中Serial对应新生代,Serial Old(后面讲)对应老年代

HotSpot虚拟机开发团队也一直为降低用户线程的停顿时间而努力,但仍然无法完全消除,可以目前最新可以做到平均10ms以内,这种停顿是完全不会影响到用户体验的,用户甚至感受不到。Serial收集器出现最早,但它就淘汰了吗?然而并没有,它依然是HotSpot虚拟机运行在客户端模式下的默认收集器,因为它简单而高效,对于单核处理器或者处理器核心数较少情况,由于Serial收集器没有线程之间交互的开销,它可以获得最高单线程收集效率,对于收集几十兆到一两百兆的新生代停顿时间完全可以控制在最多100ms以内,许多用户完全可以接受。

总结:新生代 , 标记-复制算法 , 单线程 GC , 暂停用户线程
注:标记复制算法后称复制算法

1.2 ParNew收集器

Par是Parallel(并行)的缩写,也就是可以多条GC线程同时执行,提高垃圾回收速度,这款收集器除了支持多线程并行收集,其他相比Serial收集器并没有太多创新之处,在多核处理器环境效果比较好,但是单核处理器环境绝对不会比Serial收集器更好,因为存在线程交互的开销。图中ParNew对应新生代,Serial Old对应老年代

注意:这个Parallel(并行)是指多个GC线程同时执行,同一时间多条GC线程协同工作,不要与并发(Concurrent)搞混,这里的并发是指同一时间用户线程和GC线程同时执行,而并行是只有GC线程

现代操作系统一书中的并发与并行概念:
并行:某一时刻,线程同时执行
并发:某一时段,线程交替执行(时间片轮转交替)

总结:新生代 , 复制算法 , 多线程 GC , 暂停用户线程

1.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一款新生代收集器,支持并行收集,与其他收集器的关注点不同,其他收集器可能关注怎么尽可能缩短GC时用户线程的停顿时间,而 Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。停顿时间是为了用户体验,交互多就要求低延时,而高吞吐量可以最高效率的利用处理器资源,尽快完成运算任务,适用于在后代计算而不太做交互的分析任务。由于与吞吐量关系密切,该收集器也被称为“吞吐量优先收集器

总结:新生代 , 复制算法 , 多线程 GC , 暂停用户线程 ( 关注吞吐量 )

新生代三个已经讲完了,回顾一下,然后我们就看老年代的回收器
Serial(单线程)—> ParNew(多线程)-> Parallel Scavenge(多线程关注吞吐量)

三者GC时都要暂停其他所有用户线程

1.4 Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本, 同样是一个单线程收集器, 使用标记-整理算法, 可与Serial收集器搭配使用, 现在用的比较少, 一般是老年代收集器CMS失败后, 会用这个做一个后备方案, Serial Old对应右边老年代过程

总结:老年代,单线程,标记-整理算法,暂停用户线程

1.5 Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,基于标记-整理算法,支持多线程并行收集,可和Parallel Scavenge收集器搭配使用,成为关注“吞吐量优先”名副其实的搭配

总结:老年代,标记-整理算法,多线程并发,暂停用户线程

以上都有暂停用户线程,就会有一定的停顿时间,有没有一款停顿时间较短的收集器呢?有,CMS
暂停用户线程,因为标记-清理过程要移动对象,移动对象就要停顿用户线程,可以想想你妈妈在扫地,你肯定要坐在那乖乖不动,不弄脏清扫的地方,妈妈才能扫完啊,不然一遍扫一遍丢垃圾永远清理不完。

1.6 CMS收集器

CMS(Concurrent Mark Sweep-并发标记清除)收集器是一种目标以获取最短回收停顿时间的收集器,目前许多Java系统是集中在基于浏览器B/S系统的服务端上,这类系统都比较关注响应速度,希望停顿时间短,用户体验好,点击网页能马上有对应反馈,CMS就非常符合这种需求,CMS因此也称为“并发低停顿收集器”。

CMS是基于标记-清除的算法(Mark Sweep),执行流程更为复杂,如下,分为四个步骤:
1、初始标记:仅标记与GC-Roots能直接关联到的对象,速度很快,停顿时间很短
2、并发标记:从GC-Roots直接关联对象开始遍历整个对象图,标记出所有引用链,这个过程较长,但做到了并发运行,GC线程和用户线程可以并发执行,不需要暂停
3、重新标记:修正因用户线程在并发标记过程导致引用变动的那一部分标记对象(增量更新算法),这个过程也会有停顿,但是也短
4、并发清除:清除标记的“死亡”对象,这个过程是可以与用户线程并发执行的。

注意:增量更新和后面G1中用的原始快粘(SATB)都是为了解决并发扫描时对象消失的问题,对象消失也就是用户线程工作时会修改引用关系,引用变动导致原本存活对象误标记为死亡对象,这样原本存活对象就消失了,这后果严重。为解决这个问题CMS采用增量更新,后面G1采用原始快照解决,具体这两个就不详细展开,有兴趣可以自己查阅。

整个过程中,并发标记和并发清除是最耗时的,但是相较之前的还是追求到了低停顿,因为算法采用标记-清除,避免标记清理的停顿(整理就是必须等GC线程整理完,用户线程才能恢复执行)

缺点:CMS依赖于处理器资源,并发阶段虽然不会使得用户线程停顿,但是却占用了一部分处理器资源,使得用户线程执行速度变慢,降低了总吞吐量。当处理器核心数不足4个,CMS对用户程序的执行就会影响很大。并且由于使用标记-清除算法就会产生大量空间碎片,这给需要连续空间的大对象带来麻烦,如果始终找不到连续空间,可能就会触发Full GC。

优点:实现了低停顿,高响应

总结:多线程并发(用户线程与GC线程并发),老年代,标记-清除算法

1.7 G1收集器(重点)

G1收集器是垃圾收集器技术发展历史上的里程碑成果,开创了面向局部收集的设计思路和基于Region的内存布局(后面讲),JDK 8 Update 40版本后,G1正式成熟,JDK9成为服务端模式的默认垃圾收集器,G1和之前介绍的收集器都不一样,之前的都是针对某个代,,要么新生代,要么老年代,要么就是对整个堆,但是G1不再是针对哪个代,而是面向堆内存任何部分进行收集,通俗就是哪部分内存存放垃圾最多,回收收益最大,就回收哪个,这就是G1的Mixed GC(混合收集:整个新生代和部分老年代都收集,目前只有G1有这种混合收集)。

G1设计思想:G1不再将堆分新生代和老年代,而是将连续的堆内存划分为多个大小相等的独立Region(区域),每个Region都可以扮演新生代或者老年代,G1将Region作为单词回收的最小单元,哪部分Region垃圾最多,回收收益最大,就回收它,这样G1收集器只要判断各个Region里面垃圾堆积回收的性价比,然后在后台维护一个优先级列表,每次回收收益最大的,这样不管是新创建的对象,还是存活一段时间的对象都能取得很好的收集效果,并且G1能设定允许的收集停顿时间,每次收集不能超过这个设置的停顿时间,这样就可以达到可控时延下的最大吞吐量。

G1执行过程:
1、初始标记:仅标记与GC-Roots能直接关联的对象,这里要暂停用户线程
2、并发标记:直接关联对象扫描整个堆得对象图,做可达性分析,并处理引用变动的对象
3、最终标记:暂停用户线程,处理并发阶段遗留下来的SATB(原始快照)记录,防止对象消失情况
4、筛选回收:负责统计更新各个Region的回收价值和成本,根据用户设置的期望停顿时间制定回收计划,可选多个Region构成回收集,然后开始回收,回收采用的是复制算法,将这多个Region中的存活对象复制到空的Region中,然后清除这些旧的Region全部空间,不会产生空间碎片,有利于程序长时间运行。

G1除了并发标记阶段,其他阶段都要暂停用户线程,而CMS只有初始标记和重新标记两个阶段需要暂停一会儿。所以G1并不是纯粹追求低延迟,官方目标是在延迟可控的前提下获得尽可能高的吞吐量,那我们可不可以把这个延迟设置很低呢?可以,但是会有问题,就是因为停顿时间短,导致垃圾收集器每次选出的多个Region组成的回收集就一点点,收集速度还赶不上垃圾产生的速度,久而久之,就会产生垃圾堆积。最终导致整个内存占满,触发Full GC,反而大大影响体验。所以一般设置为100ms-200ms都合理,时延也不高,吞吐量也不错,G1也是目前默认的优秀成熟的垃圾收集器。

总结:局部收集的设计思路,Region内存设计(不分代),多线程并发,标记复制算法

我们可以对这7种收集器再复习一下,Serial(单线程,新生代)-> Serial Old(单线程 Serial老年代版本)
->ParNew(多线程并行,新生代,除了并行其他和Serial差不多)-> Parallel Scavenge(多线程并行 新生代,更关注可控吞吐量)-> Parallel Old(多线程并行,老年代)->CMS(多线程并发,一般老年代,关注低时延)->G1(多线程并行与并发,Region设计,不分代)

二、2种最新的低延迟垃圾收集器

衡量垃圾收集器三个最重要指标是:内存占用、吞吐量和延迟,三者不可能都达到完美,只能在整体上达到最优,因为延迟越低,吞吐量也会降低,成反比,我们只能做到在低延迟下,如何保证不错的吞吐量,也许会多占点内存,但是由于现在硬件发展,内存加大,我们也可以容忍收集器线程多占一点点内存。但由于两个收集器还处于发展实验阶段,并没有完全成熟,但在许多场合都表现非常优秀,如后面的ZGC,可能会成为下一个默认垃圾收集器也说不定。

2.1 Shenandoah收集器

看到这个英文第一反应是真长,咋读,咱就读神呐多吧!这款收集器目标是在任何堆内存大小下都可以让垃圾收集的停顿时间都限制在10ms以下并且吞吐量还不错,,真正的低延迟,默认回收策略和G1一样,都是基于Region优先处理回收价值大的,重点改进是,实现并发整理,我们知道之前整理都是要停顿所有用户线程的,不能一边扔垃圾又产生垃圾。而这个做到了并发整理,也就是不用停顿了,这就厉害了。停止用户线程执行确实会消耗不少时间,产生时延,降低响应速度。

执行过程:
1、初始标记:与G1一样,标记与GC-Roots直接关联的对象
2、并发标记:与G1一样,遍历直接关联对象,遍历对象图,可达性分析标记所有可达对象
3、最终标记:与G1一样,处理剩余SATB(原始快照)记录,防止对象消失的后果,不过还加了一步,就是先在这个阶段统计回收价值最高的Region,组成回收集
4、并发清理:用标记-清理算法进行清理,清理那些连一个存活对象都没有的Region区域
5、并发回收:并发回收是核心改变,使得GC线程可以和用户线程并发执行,不用停顿,就是先复制回收集中存活对象到其他未使用的Region中,这里和G1筛选回收做法类似,但是筛选回收(G1的复制算法过程)必须暂停用户线程,因为用户线程工作会不断改变对象引用关系。那Shenandoah收集器的并发回收就解决了这个问题,可以做到复制过程还是一起并发执行,也就是不需要暂停(这里用到了读屏障和转发指针的方法实现,有兴趣可以去了解)
6、引用更新:回收后,引用是需要修正的,这里就是修正更新对象引用
7、再并发清理:并发回收和引用更新后,所有Region再无存活对象,然后清理选出的回收集Region空间

关键改进:做到了清理时也能并发进行,不用停顿

从结果来看,应该说2016年做该测试时的Shenandoah并没有完全达成预定的10ms以下目标,但停顿确实降低非常多,停顿时间比其他几款收集器确实有了质的飞跃,但也并未实现最大停顿时间控制在十毫秒以内 的目标,而吞吐量方面则出现了很明显的下降(运行时间体现),其总运行时间是所有测试收集器中最长的。

总结:超低延迟(10ms以下),并发清理(亮点),标记复制算法

2.2 ZGC收集器(重点)

ZGC(Z Garbage Collector)目标和 Shenandoah收集器一样,都希望尽可能对吞吐量影响不大前提下,实现任意堆内存大小垃圾收集都维持在10ms以下的超低延迟,ZGC也是基于Region内存布局,似乎现在优秀的收集器都设计为Region区域内存布局,没有再设计成分代,当然不是分代就不行,只是现在的设计方向,ZGC的Region更厉害具有动态性–动态创建与销毁+动态区域容量。它的Region可分为大中小三类:

  • 小型Region(Small Region):容量固定为2MB,用于放置小于256KB的小对象。
  • 中型Region(Medium Region):容量固定为32MB,用于放置大于等于256KB但小于4MB的对 象。
  • 大型Region(Large Region):容量不固定,可以动态变化,但必须为2MB的整数倍,用于放置 4MB或以上的大对象。每个大型Region中只会存放一个大对象

ZGC的核心变化:并发整理算法的实现,虽然 Shenandoah收集器也实现了并发整理,但ZGC解决这个问题的方法设计的更为复杂精巧,用到了染色指针(标志性设计),读屏障,内存多重映射,有兴趣可以了解下染色指针,篇幅有限不展开

执行过程:
1、并发标记:一样遍历图做可达性分析,标记所有存活对象,这里有个不同就是ZGC的标记是在指针上进行,而不是对象
2、并发预备重分配:扫描所有Region,查出需要清理的Region,构成分配集,然后复制存活对象到其他空的Region,再释放旧Region中死亡对象
3、并发重分配(核心):为每个Region维护一个转发表,用于记录从旧对象到新对象的转向关系,也就是引用,这样可以通过转发表就能访问到新复制对象,以前还需要进行修正引用,防止对象消失情况,现在有转发表就行。
4、并发重映射:重映射就是修正整个堆中指向重分配集中旧对象的所有引用,通过转发表就能直接修正

ZGC尽管还在实验阶段,还没有完成所有特性,稳定性与性能调优也在进行,但是当前状态的ZGC性能表现已经相当亮眼,甚至在ZGC弱项“吞吐量”方面(主打超低延迟10ms以下,且对吞吐量影响不大),低延迟为主要目标的ZGC已经达到了以高吞吐量为目标Parallel Scavenge 的99%,直接超越了G1。以后等ZGC成熟也许会代替G1成为下一个里程碑式的默认垃圾收集器,但现在还在实验发展阶段,不敢说稳定性100%好。

总结:并发清理(更精巧),做到超低延迟(10ms)下,吞吐量也挺不错,标记复制算法

以上就是7+2的垃圾回收器,目前懂这些完全够了

以上是关于《深入理解Java虚拟机系列三》--- 7+2种垃圾收集器(通俗易懂)的主要内容,如果未能解决你的问题,请参考以下文章

深入理解JVM读书笔记三: 虚拟机类加载机制

《深入理解Java虚拟机系列二》--- 垃圾回收算法(通俗易懂)

Java虚拟机系列(25篇文章)一起啃

深入理解java虚拟机系列:java内存区域与内存溢出异常

JVM | 第2部分:虚拟机执行子系统《深入理解 Java 虚拟机》

深入理解Java虚拟机 -- 虚拟机类加载机制