JVM - 内功修炼之垃圾收集器

JVM - 内功修炼之垃圾收集器

1.垃圾收集器概述

 如果说上一篇讲的垃圾回收算法是内存回收的方法论,那这一篇我们要介绍的垃圾收集器就是内存回收的具体实现。

 Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。

 我们需要明确一个观点,我们接下来会对一部分垃圾收集器进行介绍以及比较,但目的并不是为了筛选出最好的垃圾收集器。因为目前为止并没有一款最好的垃圾收集器可以同时满足所有场景,都需要根据实际情况进行选择。如果目前有这么一款完美的垃圾收集器的话,那各大厂商也不会实现那么多不同的垃圾收集器了。

2.Serial垃圾收集器

在这里插入图片描述

2.1 Serial垃圾收集器是什么?

Serial垃圾收集器是最基本、发展历史最悠久的收集器。这个收集器是一个单线程的垃圾收集器,它不仅仅在进行垃圾回收时只会使用单个线程去回收。如上图所示,当达到一个安全点时会触发GC线程,此时Serial垃圾收集器开始工作,并且在回收的同时会将其他所有工作线程暂停,直到回收结束。这就是大名鼎鼎“Stop The World”-STW

2.2 Serial垃圾收集器的特点

 这种收集器在思路上比较简单但是在很多时候也比较实用,不然也不会在某种情况下作为默认收集器。这里我们可以总结出几个特点:

  1. “Stop The World(STW)”:当它进行垃圾回收时,必须暂停其他所有的工作线程,直到它收集结束。
  2. 效率:由于采用单线程处理垃圾回收,无需过多的线程交互,简单高效。当JVM管理小数据量内存(新生代几十到一两百M)时,停顿时间可以控制在几十毫秒最多一百多毫秒内,也可通过-XX:+UseSerialGC配置该垃圾收集器。
  3. 使用场景:多用于桌面应用,Client端的垃圾回收器,由于桌面应用内存小,进行垃圾回收的时间比较短,只要不频繁发生停顿就可以接受。

3.ParNew垃圾收集器

在这里插入图片描述

3.1 ParNew垃圾收集器是什么?

ParNew收集器其实就是上面Serial收集器的多线程版本,除了使用多条线程秉性进行垃圾收集之外,其余包括Serial收集器可用的所有控制参数、收集算法、Stop The World(STW)、对象分配规则、回收策略等都与Serial收集器完全一致。在实现上,这两种收集器也共用了相当多的代码。

3.2 ParNew垃圾收集器的特点

  1. ParNew收集器除了多线程并行收集之外,其他与Serial收集器相比并没有太大差异,但它却是许多运行在 Server模式下的虚拟机中首选的新生代收集器。其中有一个与性能无关但很重要的原因就是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。
  2. 可以使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数。
  3. 由于存在线程切换的开销,ParNew收集器在单CPU的环境中比不上Serial收集器。但随着可用的CPU数量的增加, 收集效率肯定也会大大增加,建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产生上下文切换消耗。

3.3 并行和并发

 为了接下来各个垃圾收集器的介绍更加顺利,这里先给大家简单接受两个概念:并行和并发。

  1. 并行(Parallel):多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  2. 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在保持运行,而垃圾收集程序运行于另一个CPU上。

4.Parallel Scavenge垃圾收集器

4.1 Parallel Scavenge垃圾收集器是什么?

Parallel Scavenge收集器是一个新生代收集器,同样也是一种使用复制算法、并行的多线程垃圾收集器。和ParNew收集器不同的是Parallel Scavenge更关注吞吐量,由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。

 那么吞吐量是什么呢?即CPU用于运行用户代码的时间与CPU总时间的比值,假设99分钟时间用于执行用户线程,1分钟时间用于回收垃圾 ,此时吞吐量就是99%。

4.2 Parallel Scavenge垃圾收集器是特点

  1. Parallel Scavenge收集器的特点主要是其关注点与其他收集器不同,上述其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。

    上面我们也提到了,所谓吞吐量就是CPU用于执行用户代码时间与CPU总消耗时间的比值,即吞吐量=执行用户代码时间/(运行用户代码时间+垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的场景。
  2. 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应调节策略。
  3. 可以通过-XX:MaxGCPauseMillis参数修改GC停顿时间,当然并不会越小越好,如果我们将该参数设置太小,每次GC清除的内存空间就会变少,则会导致程序频繁发生GC。停顿时间虽然减少了,但是吞吐量也下降了。
  4. 可以通过-XX:GCTimeRatio参数参数修改垃圾收集时间占总时间比例,相当于吞吐量倒数。例如设置为19则表示允许最大GC时间占总时间比5%,即1/(1+19)。默认为99,则允许最大GC时间为1%,即1/(1+99)。

5.Serial Old垃圾收集器

在这里插入图片描述

5.1 Serial Old垃圾收集器是什么?

 记下来要介绍的这两个垃圾收集器因为就是在之前介绍的基础上有一些简单的区别,所以这里会简单介绍下。

Serial Old收集器其实就是Serial收集器的老年代版本,主要意义也是给Client模式下使用。它也是一个单线程收集器,并且采用标记-整理算法。当然也可以被选作Server模式使用,主要作用有两点:

  1. 在JDK1.5及之前版本可能会被选择与Parallel Scavenge收集器搭配使用。
  2. 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

6.Parallel Old垃圾收集器

在这里插入图片描述

6.1 Parallel Old垃圾收集器是什么?

 跟Serial Old类似,Parallel Old收集器其实就是Parallel Scavenge收集器的老年代版本。采用多线程+标记-整理算法

 上面我们也提到了,JDK1.5及之前版本Parallel Scavenge收集器一般都和Serial Old收集器是黄金搭档。一个主要的原因就是由于Parallel Scavenge无法和CMS等收集器配合,这个后面讲解CMS收集器时会提到。而由于Serial Old收集器在服务端应用的表现并不理想,所以Parallel Scavenge并不能在整体应用上体现吞吐量最大化的效果。在某些老年代空间较大并且硬件高级(CPU处理能力强劲)的环境下,这对黄金搭档的吞吐量可能还不及ParNew+CMS给力。

 从JDK1.6开始,Parallel Old登上了舞台。此时吞吐量优先的收集器才有了名副其实的名头,在注重吞吐量和CPU资源敏感的场景下,都可以优先考虑Parallel ScavengeParallel Old收集器的组合。

7.CMS垃圾收集器(Concurrent Mark Sweep)

7.1 CMS垃圾收集器是什么?

 看到这里,相信各位对JVM中的垃圾收集器已经有了一个不错的认识了。结合之前介绍的垃圾回收算法,攻破垃圾收集器实在不在话下。

 那么CMS垃圾收集器是什么?这是一种以最短回收停顿时间为核心目标的收集器,基于标记-清除算法实现的。

 目前很大一部分的Java应用集中在服务端模式上,这类服务十分重视服务的响应速度,停顿时间越短给用户带来的体验越好。而CMS收集器就非常符合这种场景。

7.2 CMS垃圾收集器的步骤流程

在这里插入图片描述

 从上图我们看到,CMS收集器进行垃圾回收主要有4个步骤:

  1. 初始标记(Initial Mark):仅仅标记一下GC Roots能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记(Concurrent Mark):并发标记阶段就是进行GC RootsTracing,也就是标记引用链的过程,它在整个回收过程中耗时最长,但不需要停顿。
  3. 重新标记(Remark):用于修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  4. 并发清除(Concurrent Sweep):清除标记内存空间,不需要停顿。


 可以看出整个过程中耗时最长的两个阶段是并发标记并发清除,而此时GC线程是可以与用户线程并发工作的,所以并不会产生停顿。

7.3 CMS垃圾收集器的优缺点

 上面我们所了解了这么多垃圾收集器,其实比对可以发现,CMS已经算得上是一款优秀的垃圾收集器了。它的优点也很明显:并发标记并发收集低停顿。不过仅凭这些,CMS还远达不到完美的程度,主要是因为这种设计存在3个明显的缺点:

  1. 首先从CMS收集器的设计上我们就能很清楚的感受到,它对CPU资源其实是十分敏感的。在并发标记并发删除阶段,虽然不会使用户线程停顿,但是自身GC线程会占用一部分资源从而导致用户线程性能下降,吞吐量降低。CMS默认启动的回收线程数=(CPU数量+3)/4,当CPU数量很少时,还要分出一大部分资源来同时进行垃圾回收,其实对性能的影响也是十分明显的。

    可以看出低停顿时间是以牺牲吞吐量为代价的。由于这种设计,其实所有面向并发设计程序对于CPU资源都是十分敏感的。

  1. 我们可以在脑海中回想一下刚才我们所了解的CMS收集器的整个垃圾回收过程。当我们在并发清除阶段,其实用户线程依然是在执行的。而此时仍然会不断有新的垃圾产生,这些新产生的垃圾并未被标记,所以这一部分垃圾会在下一个GC周期里面被清除,这部分垃圾就叫做浮动垃圾(Floating Garbage)

    由于浮动垃圾的存在,因此需要预留一部分内存空间给用户线程。当预留内存不足以存放浮动垃圾时,则会出现Concurrent Mode Failure,此时虚拟机将会临时启用Serial Old收集器来重新进行老年代的垃圾回收。如果我们应用中老年代增速不是太快,可以通过-XX:CMSInitiatingOccupancyFraction来提高GC触发百分比,以降低内存回收次数从而提升性能;但若将此参数设置太高,可能会出现大量的Concurrent Mode Failure导致性能反而降低。

  1. 不知大家是否还得之前我们讲过的,CMS收集器基于标记-清除算法,那么意味着在GC后有可能会产生大量的空间碎片。若空间碎片过多的话,将会对大对象的分配造成影响,时常会出现老年代中还有不少空间剩余但却无法为大对象分配内存,从而不得不提前触发Full GC

    针对这个问题,CMS收集器其实是提供一个内存碎片合并整理的机制,可以通过-XX:+UseCMSCompactAtFullCollection参数开启或关闭(默认开发)。该机制会在上面提及情况触发Full GC时对内存碎片进行合并整理,但是这个过程是无法并发的,所以虽然解决了空间碎片问题,但也带来了停顿时间增长的问题。另外我们还可以通过调整-XX:CMSFullGCsBeforeCompaction参数去控制每执行多少次不压缩的Full GC后会从而进行一次空间碎片合并整理。

8.G1垃圾收集器(Garbage-First)

8.1 G1垃圾收集器是什么?

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,在多CPU和大内存的场景下都有很好的性能。这款垃圾收集器是目前为止在垃圾收集器技术发展上都是比较前沿的成果之一。在JDK7-u4版本起就已经完全支持G1垃圾收集器了,而HotSpot开发团队赋予给它的使命也是在未来可以替换掉CMS

 我们都知道堆被分为新生代和老年代,对于我们之前所了解的其他收集器来说都是针对整个新生代或者整个老年代进行垃圾回收,而G1的设计可以对整个新生代以及老年代一起进行回收。官方也提供了【G1GettingStarted】可供我们对G1进行进一步的了解。

8.2 G1垃圾收集器的特点

在这里插入图片描述

 众所周知我们所了解老的收集器将堆分为三个固定内存大小的新生代老年代永久代。所有内存对象最终都属于这三个部分之一。而G1收集器采用了不同的放划分方式。
在这里插入图片描述
G1收集器将整个Java堆划分为N个大小相等的独立区域(Region),也就是说新生代和老年代在某种意义上来说不再是物理隔离的。

 这种划分方式,由于每个独立区域的大小并非固定,使得内存在使用方面有了更大的灵活性。而每个Region都有一个与之对应的Remembered Set,用来记录该Region对象的引用对象所在的Region,这样做可以避免在做可达性分析的时进行全堆扫描。

 而正是由于其设计特点,也就有了其特有的优势,这些下面我们会提到。

8.3 G1垃圾收集器的步骤流程

在这里插入图片描述

 从上图我们看到,G1收集器进行垃圾回收主要有4个步骤:

  1. 初始标记(Initial Mark):仅仅标记一下GC Roots能直接关联到的对象,速度很快,需要停顿。
  2. 并发标记(Concurrent Mark):并发标记阶段就是进行GC RootsTracing,也就是标记引用链的过程,它在整个回收过程中耗时最长,但可与用户程序并发执行不需要停顿。
  3. 最终标记(Final Marking):用于修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
  4. 筛选回收(Live Data Counting And Evacuation):先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率,故默认停顿并行回收。

8.4 G1垃圾收集器的优缺点

 上面我们对G1收集器独有的特点以及其运行流程都有了一个比较清晰的认识,我们会发现这款垃圾收集器其实结合了我们上面所了解收集器的大部分优点以及针对很多其他收集器有的缺点都提供了具体的解决方案。这里我们来看看G1收集器几个主要的优点:

  1. 并行与并发:这一点不用多说,作为官方希望取代CMS的一款收集器,G1收集器能够充分利用多CPU、多核的硬件优势,尽可能地去缩短停顿时间。并且仍可选择是否需要并发执行用户线程。
  2. 空间整合:整体基于标记-整理算法实现,局部Region采用复制算法实现,这两种算法都可使程序在运行期间不会产生内存空间碎片。
  3. 可预测的停顿G1收集器除了追求低停顿外,还能够建立可预测的停顿时间模型。在使用的过程中,我们可以指定在一个长度X毫秒的时间片段内,消耗在GC上的时间不得超过Y毫秒。

    G1收集器之所以能够建立可预测的停顿时间模型,和其独特的堆区域划分密不可分。它会记录每个Region中垃圾堆积的价值(回收所需时间以及回收所得空间),并通过其数值维护一个优先级列表,每次根据允许的手机时间优先回收价值最大的RegionGarbage First也因此得名。这种方式也保证了G1收集器在有限时间内可以保证尽可能高的收集效率。

 了解过G1垃圾收集器的同学会发现一个现象,那就是在大多数书籍或者网上相关技术介绍都很少会提到G1的缺点。主要原因还是G1收集器虽然被官方计划是替代CMS的选择,但其在商用层面的案例还是很少的,并没有表现出足够的性能优势。

G1收集器得不到广泛商用的原因就和我们使用JDK等一样,大部分项目我们都会尽可能去保证在一个稳定的情况下运行,并不会过于激进在真实项目中去尝试使用过于新的技术。不过相信在不久的将来,若G1收集器不断优化优势越加明显后就会被大家广泛接受推广。

8.5 G1垃圾收集器的应用场景

G1主要的应用场景就是针对需要运行堆内存较大并且GC延迟有限的应用程序,官方介绍说在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒。

 在很多情况下,应用程序可能搭配的是JDK1.5提供的CMS收集器,而我们在以下场景可以尝试替换为G1收集器:

  1. 完整GC时间过长或过于频繁(大于0.5s-1s)。
  2. 对象分配频率或年代提升频率变化较大。
  3. 堆内存中活动数据占用过多。

 也就是说如果我们程序未经理长时间的垃圾收集停顿,可以考虑使用原有ParallelOldCMS收集器。而且是否需要采用G1替代原有收集器也需要我们经过实际场景验证才可以下定论。

8.6 G1垃圾收集器的参数配置

 既然我们要使用G1收集器,那就需要对几个简单的参数配置做基本的了解:

  1. -XX:+UseG1GC:该参数用于指定使用G1收集器。
  2. -XX:MaxGCPauseMillis:设置G1收集器停顿时间,默认200ms。
  3. -XX:G1HeapRegionSize:设置单个Region大小(1MB-32MB),默认会根据堆大小计算最优值。
  4. -XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段。默认为45。

9.总结

 关于JVM中垃圾收集器到这里就告一段落了,我们这里将大部分垃圾收集器都做了一个介绍,我们对每种收集器的特点以及流程也都有了一个大致的认识。不过这些知识对于整个JVM还只是冰山一角,但是当我们在实际开发中遇到一些JVM内存相关的问题时还是可以在一定程度上避免我们毫无头绪的。

 当然,对于大多数情况以咱们了解的这些还是能够作为敲门砖摸到一些门路去想办法找到问题根源的。不过这里还是不建议大家在了解片面的情况下就将以往的收集器给随意替换掉的,任何工具都有其适用的场景,适合你的不一定是最好的,最好的也不一定是适合你的。

 最后还是希望这些能够给大家带来帮助。这里正逢2020到来,祝大家新年快乐,愿大家只争朝夕,不负韶华

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值