(七)Java垃圾收集器详解

面试官问:Java垃圾收集器了解过多少,说一下 JVM 有哪些垃圾回收器?这些问题在你面试高级Java的时候经常会问到。本篇文章结合着【深入理解Java虚拟机】一书当中整理了本篇博客。

如果想要对收集器了解的更深,建议一点一点读,如果想大概了解一下,只是为了面试问到可以简单说一下,那么可以直接看我下方写的总结,总结相对来说内容不是很多,但是每个收集器优缺点都整理到了。

JVM的知识是连贯性的,如果你连Java内存分布,以及垃圾回收相关知识都不知道,建议您不要读这篇文章,读起来会让你失去学习的兴致。可以先去读读我前面所写的JVM相关知识。

一、概述

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。

Java垃圾收集器这个不学就不能工作吗?答案是不是,这个代表你掌握JVM的深度。Java离开JVM就不行吗,答案是肯定的,这个问就好似程序员离开电脑能行吗,之前我是深深体会到JVM不学好,会吃多大的亏。项目内存出现瓶颈问题根本无从下手,这都是需要依靠这些知识的积累,才能全方面的去解决JVM优化问题。

我们都知道Java默认的虚拟机类型是HotSpot虚拟机,HotSpot虚拟机有多种不同的收集器,既然实现这么多种,意味着每一种都有每一种的作用。针对这一点,我们进行深入学习Java垃圾收集器。

垃圾收集器是干什么的?为什么要了解他呢?

很多人其实对这块根本不理解,只知道面试的时候会经常问垃圾回收算法,而且还知道有三种,标记清除法、标记复制法、标记整理法,如果再问你,虚拟机用的哪个算法你还知道吗?答案是,三种都用到了。

各款经典收集器之间的关系 如图所示。
在这里插入图片描述
七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配 使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。接下来笔者将 逐一介绍这些收集器的目标、特性、原理和使用场景,并重点分析CMS和G1这两款相对复杂而又广泛 使用的收集器,深入了解它们的部分运作细节。

虽然垃圾收集器的技术在不断进步,但直到现在还没有 最好的收集器出现,更加不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。

二、Java垃圾收集器

2.1、Serial收集器

Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK 1.3.1之前是HotSpot虚拟机新生代 收集器的唯一选择。大家只看名字就能够猜到,这个收集器是一个单线程工作的收集器,但它的“单线 程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强 调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”这个词语也 许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况 下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。读者不妨试想一下,要是 你的电脑每运行一个小时就会暂停响应五分钟,你会有什么样的心情?下图示意了Serial/Serial Old收 集器的运行过程。

在这里插入图片描述
为什么要停止所有用户线程?

对于“Stop The World”带给用户的恶劣体验,早期HotSpot虚拟机的设计者们表示完全理解,但也 同时表示非常委屈:因为你要进行清理的时候,你正清理着,还有用户线程在造着,这清理到什么时候是个头。

从JDK 1.3开始,一直到现在最新的JDK 13,HotSpot虚拟机开发团队为消除或者降低用户线程因 垃圾收集而导致停顿的努力一直持续进行着,从Serial收集器Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1)收集器,最终至现在垃圾收集器的最前沿成果ShenandoahZGC 等,我们看到了一个个越来越构思精巧,越来越优秀,也越来越复杂的垃圾收集器不断涌现,用户线 程的停顿时间在持续缩短。

迄今为止,Serial收集器依然是HotSpot虚拟机运行在客户端模式下的默认新生 代收集器

什么是Java客户端?

客户端就是指的的桌面可以直接点击的应用,有的会直接在客户端里面集成JDK,用户便可以点击直接运行,当关闭应用的时候,就相当于JVM停止,点击应用的时候,JVM开始运行,所以一般客户端的JVM我们要的是单线程性价比最高的。

优点:

  1. 简单而高效(与其他收集器的单线程相比)
  2. 它是所有收集器里额外内存消耗(Memory Footprint)最小的
  3. 对于单核处理 器或处理器核心数较少的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以 `获得最高的单线程收集效率

在用户桌面的应用场景以及近年来流行的部分微服务应用中,分配给虚 拟机管理的内存一般来说并不会特别大,收集几十兆甚至一两百兆的新生代,垃圾收集的停顿时间完全可以控制在十几、几十毫秒,最多一 百多毫秒以内,只要不是频繁发生收集,这点停顿时间对许多用户来说是完全可以接受的。所以,Serial收集器对于运行在客户端模式下的虚拟机来说是一个很好的选择。

2.2、ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。ParNew收 集器的工作过程如图所示。

在这里插入图片描述

ParNew在JDK7之前是服务端模式下的HotSpot虚拟机 首选的新生代收集 器,原因:除了Serial收集器外,目前只有它能与CMS 收集器配合工作。

在JDK 5发布时,HotSpot推出了一款在强交互应用中几乎可称为具有划时代意义的垃圾收集器 ——CMS收集器。这款收集器是HotSpot虚拟机中第一款真正意义上支持并发的垃圾收集器,它首次 实现了让垃圾收集线程与用户线程(基本上)同时工作

遗憾的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作,所以在JDK 5中使用CMS来收集老年代的时候新生代只能选择ParNew或者 Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默 认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。

可以说直到CMS的出现才巩固了ParNew的地位,但成也萧何败也萧何,随着垃圾收集器技术的不 断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场G1是一个面向全堆的收集器,不 再需要其他新生代收集器的配合工作。所以自JDK 9开始ParNew加CMS收集器的组合就不再是官方 推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加 Serial Old以及Serial加CMS这两组收集器组合的支持(其实原本也很少人这样使用),并直接取消了- XX:+UseParNewGC参数,这意味着ParNew和CMS从此只能互相搭配使用,再也没有其他收集器能 够和它们配合了。读者也可以理解为从此以后,ParNew合并入CMS,成为它专门处理新生代的组成部 分。ParNew可以说是HotSpot虚拟机中第一款退出历史舞台的垃圾收集器。

ParNew收集器在单核处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程 交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分之百保证超越Serial收集器。当然,随着可以被使用的处理器核心数量的增加,ParNew对于垃圾收集时 系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数

注意 从ParNew收集器开始,后面还将会接触到若干款涉及“并发”和“并行”概念的收集器。 在大家可能产生疑惑之前,有必要先解释清楚这两个名词。并行和并发都是并发编程中的专业名词, 在谈论垃圾收集器的上下文语境中,它们可以理解为:

  • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线 程在协同工作,通常默认此时用户线程是处于等待状态。

  • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾 收集器线程与用户线程都在运行。由于用户线程并未被冻结,所以程序仍然能响应服务请求,但由于 垃圾收集器线程占用了一部分系统资源此时应用程序的处理的吞吐量将受到一定影响

Parallel Scavenge收集器及 后面提到的G1收集器等都没有使用HotSpot中原本设计的垃圾收集器的分代框架,而选择另外独立实 现。Serial、ParNew收集器则共用了这部分的框架代码
详细可参考: https://blogs.oracle.com/jonthecollector/our_collectors。

2.3、Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是 能够并行收集的多线程收集器……Parallel Scavenge的诸多特性从表面上看和ParNew非常相似,那它有 什么特别之处呢?

Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能 地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值, 即

可以的当做是一个输入和输出的比值。

在这里插入图片描述
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分 钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良 好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算 任务,主要适合在后台运算而不需要太多交互的分析任务。

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

  • -XX:MaxGCPauseMillis :控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio :设置吞吐量大小
  • -XX:+UseAdaptiveSizePolicy :这是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时 间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomics)。

-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的 时间不超过用户设定值。不过大家不要异想天开地认为如果把这个参数的值设置得更小一点就能使得 系统的垃圾收集速度变得更快垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的: 系统把新生代调得小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得 更频繁,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间 的确在下降,但吞吐量也降下来了。

-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数也就是垃圾收集时间占总时间的 比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大垃圾收集时间就占总时间的5% (即1/(1+19)),默认值为99,即允许最大1%(即1/(1+99))的垃圾收集时间。

优点:

对于收集器运作不太了解,手工优化存在困难的话,使用Parallel Scavenge收集器配合自适应调节策 略,把内存管理的调优任务交给虚拟机去完成也许是一个很不错的选择。只需要把基本的内存数据设 置好(如-Xmx设置最大堆),然后使用-XX:MaxGCPauseMillis参数(更关注最大停顿时间)或- XX:GCTimeRatio(更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就 由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器区别于ParNew收集器的一个重要特性

官方介绍:http://download.oracle.com/javase/1.5.0/docs/guide/vm/gc-ergonomics.html。

2.4、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。这个收 集器的主要意义也是供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用 途:

  • JDK 5以及之前的版本中与Parallel Scavenge新生代收集器搭配使用
  • 作为CMS 收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。这两点都将在后面的 内容中继续讲解。Serial Old收集器的工作过程如图所示。
    在这里插入图片描述
    需要说明一下,Parallel Scavenge新生代收集器架构中本身有PS MarkSweep收集器来进行老年代收集,并非 直接调用Serial Old收集器,但是这个PS MarkSweep收集器与Serial Old的实现几乎是一样的,所以在官 方的许多资料中都是直接以Serial Old代替PS MarkSweep进行讲解,这里笔者也采用这种方式。

2.5、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本支持多线程并发收集,基于标记-整理算法实 现。这个收集器是直到JDK 6时才开始提供的

在此之前,新生代的Parallel Scavenge收集器一直处于相 当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于 老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上 获得吞吐量最大化的效果。

同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处 理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,Parallel Scavenge新生代+老年代Serial Old这种组合的总吞吐量甚至不一 定比ParNew加CMS的组合来得优秀

直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重 吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组 合。Parallel Old收集器的工作过程如图所示。

在这里插入图片描述

2.6、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很 大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。CMS收集器就非 常符合这类应用的需求。

从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于标记-清除算法实现的,它的运作 过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:

  1. 初始标记(CMS initial mark) :初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记(CMS concurrent mark) :并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
  3. 重新标记(CMS remark) :重 新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一 些,但也远比并发标记阶段的时间短;
  4. 并发清除(CMS concurrent sweep):并发清除阶段,清理删除掉标记阶段判断的已经死亡的 对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。`;

通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。

在这里插入图片描述
CMS是一款优秀的收集器,它最主要的优点在名字上已经体现出来:并发收集、低停顿,一些官 方公开文档里面也称之为“并发低停顿收集器”(Concurrent Low Pause Collector)。CMS收集器是 HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的 缺点:

缺点一:CMS收集器,虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计 算能力)而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量 +3)/4,也就是只占用不超过25%的 处理器运算资源。处理器核心数量不足四个时, CMS对用户程序的影响就可能变得很大。就可能导致用户程序的执行速度忽然大幅降低。

为了缓解这种情况,虚拟机提 供了一种称为“增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)的CMS收集器变种,他是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的 时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些。这就好比长痛不如短痛,交替执行,用户访问会长时间一直卡顿,尽可能不会说直接卡死,但是资源有限,访问会很慢。从 JDK 7开始,i-CMS模式已经被声明不再提倡用户使用。

缺点二:CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。

什么是浮动垃圾?

在CMS的并发标记和并发清理阶 段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分 垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集 时再清理掉。这一部分垃圾就称为“浮动垃圾。

浮动垃圾带来的危害?

同样也是由于在垃圾收集阶段用户线程还需要持续运 行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待 到老年代几乎完全被填满了再进行收集。

JDK 5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果 在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccu-pancyFraction的值 来提高CMS的触发百分比,降低内存回收频率,获取更好的性能。

到了JDK 6时,CMS收集器的启动 阈值就已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS运行期间预留的内存无法满 足程序分配新对象的需要,就会出现一次“并发失败”(Concurrent Mode Failure),这时候虚拟机将不 得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集, 但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraction设置得太高将会很容易导致 大量的并发失败产生,性能反而降低,用户应在生产环境中根据实际应用情况来权衡设置。

缺点三:CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间 碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找 到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。

为了解决这个问题, CMS收集器提供了一个-XX:+UseCMS-CompactAtFullCollection开关参数(默认是开启的,此参数从 JDK 9开始废弃),用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程,由于这个 内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解 决了,但停顿时间又会变长.

因此虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore- Compaction(此参数从JDK 9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量 由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理(默认值为0,表 示每次进入Full GC时都进行碎片整理)。

2.7、Garbage First收集器

Garbage First(简称G1)收集器被Oracle官方称为“全功能的垃圾收集器”。

JDK 9发布之 日G1宣告取代Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器,而CMS则 沦落至被声明为不推荐使用(Deprecate)的收集器。

在G1收集器出现之前的所有 其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老 年代(Major GC),再要么就是整个Java堆(Full GC)。

G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),Region中还有一类特殊的Humongous区域,专门用来存储大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize设 定。对于那些超过了整个Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代 的一部分来进行看待,如下图所示。

用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis指定,默 认值是200毫秒)

既然有这个参数那说明他有控制停顿时间的功能,他是怎么控制的?

因为它将Region作 为单次回收的最小单元,G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小 (价值即回收所获得的空间大小以及回收所需时间的经验值),然后在后台维护一 个优先级列表,根据用户设置的时间,优先处理回收价值收益最大的那些Region,这也就是“Garbage First”名字的由来。
这里可以这么理解:他把内存分成了多个小块,每个块占用的内存给记录起来了,根据用户设置的可停顿大小来决定回收块的大小。

在这里插入图片描述
将Java堆分成多个独立Region后,Region里面存在的跨Region引用对象如何解决?

使用记忆集避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集,使用到了双向卡表(卡表是“我指向谁”,这种结构还记录了“谁指向我”)。Region数量比传统收集器的分代数量明显要多得多,故此G1至少要耗费大约相当于Java堆容量10%至20%的额 外内存来维持收集器工作。

在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?

这里首先要解决的是用户线程改变对象引用关系时,必须保证其不能打破原本的对象图结构,导致标记结果出现错误,CMS收集器采用增量更新算法实现,而G1 收集器则是通过原始快照(SATB)算法来实现的。G1把Region中的一部分空间划分出来用于并发回收过 程中的新对象分配,这部分空间不纳入回收范围。与CMS中类似,如果内存回收的速度赶不上内存分配的速度, G1收集器也要被迫冻结用户线程执行,导致Full GC而产生长时间“Stop The World”。

如果我们不去计算用户线程运行过程中的动作(如使用写屏障维护记忆集的操作),G1收集器的 运作过程大致可划分为以下四个步骤:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。

  • 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。

  • 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。

  • 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。

在这里插入图片描述

从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐 量,所以才能担当起“全功能收集器”的重任与期望。

毫无疑问,可以由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿 时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。它默认的停顿目标为两百毫秒,如果我们把停顿时间调得非常低,譬如设置为二十毫秒,很可能出现的结 果就是由于停顿目标时间太短,导致每次选出来的回收集只占堆内存很小的一部分,收集器收集的速 度逐渐跟不上分配器分配的速度,导致垃圾慢慢堆积。所以通常 把期望停顿时间设置为一两百毫秒或者两三百毫秒会是比较合理的。

从G1开始,不追求一次把整个Java堆全部清理干净。只要收集的速度能跟得上对象分配的速度,那一切就能运作得很完美。

G1和CMS区别:

  1. G1是分Region的内存布局、CMS采用分代
  2. CMS“标记-清除”算法,G1基于“标记-整理”算法实现的收集器
  3. G1每个Region都有一个记忆集,存储对象的引用情况,主要是防止跨区引用。Region比较多,内存占用自然大。CMS的记忆集只有一个,只需要处理老年代到新生代的引用。

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其 优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间,当然,以上这些也仅是经验之谈,不 同应用需要量体裁衣地实际测试才能得出最合适的结论,随着HotSpot的开发者对G1的不断优化,也 会让对比结果继续向G1倾斜。

三、总结

Java垃圾收集器也是在不断的进化当中,从第一版Serial单线程收集器,再到ParNew收集器,再到吞吐量优先Parallel Scavenge收集器,再到以获取最短回收停顿时间为目标的CMS收集器,再到G1全功能的垃圾收集器。其实很多情况下,我们对收集器是无感知的。甚至很少一部分才会去真正了解收集器,甚至只知道垃圾回收是JVM做的,其他不关我的事。

我个人感觉收集器就好比手机,虽然我们人人在用,但是根本没人关心他到底是怎么做到的。不说别的,在了解完收集器之后,最起码能让你对JVM的了解更上一个档次,在面试的时候也会让自己游刃有余。

3.1、JVM默认用的哪个收集器?

查看当前JVM的垃圾收集器
cmd中输入以下命令:

java -XX:+PrintCommandLineFlags -version

在这里插入图片描述

JDK1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
JDK1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
JDK1.9 默认垃圾收集器G1

在jdk8可以切换收集器吗?

可以,通过-XX:+UseG1GC便可以切换G1收集器。但是一般情况下不要切换,JDK8默认用了Parallel收集器,那他一定有他的原因,不管是版本兼容还是什么问题,总之肯定有他的原因,所以一般情况下不建议自己更改收集器,采用默认的即可。真正需要优化了,可以选择更改Parallel收集器的参数,例如新生代老年代大小等等。

在这里插入图片描述

3.2、Serial收集器

特点

  1. JDK 1.3.1之前是HotSpot虚拟机新生代 收集器的唯一选择
  2. 采用了整理复制算法
  3. 单线程工作的收集器

优点

  • 占用内存少,适合用在客户端,客户端一般是单线程并且占用内存少,而Serial收集器完全可以满足。

缺点

  • 进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束

3.3、ParNew收集器

特点

  1. 在JDK7首选的新生代收集器
  2. ParNew收集器实质上是Serial收集器的多线程并行版本
  3. 采用了整理复制算法
  4. 能与CMS 收集器配合工作

优点

  • 能与CMS 收集器配合工作

缺点

  1. 进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束
  2. ParNew收集器在单核处理器的环境中绝对不会比Serial收集器有更好的效果,反而会有线程切换开销。

3.4、Parallel Scavenge收集器

特点

  1. 新生代收集器
  2. 基于标记-复制算法
  3. 目标是达到一个可控制的吞吐 量

优点

  • 提供了两个参数可以控制停顿时间还有吞吐量大小,相比用其他的收集器,假如不懂收集器机制调优很困难,而Parallel Scavenge收集器,我们只需要设置这两个参数再加上堆大小,其他的交给虚拟机来控制即可。

缺点

  • 控制停顿时间,就是将新生代调小,反而会导致频繁GC

可控制参数

  • -XX:MaxGCPauseMillis :控制最大垃圾收集停顿时间
  • -XX:GCTimeRatio :设置吞吐量大小
  • -XX:+UseAdaptiveSizePolicy :这是一 个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)Eden与Survivor区 的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数 了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时 间或者最大的吞吐量

3.5、Serial Old收集器

  1. Serial Old是Serial收集器的老年代版本
  2. 标记-整理

3.6、Parallel Old收集器

  1. Parallel Old是Parallel Scavenge收集器的老年代版本
  2. 标记-整理
  3. Parallel Scavenge新生代收集器,当时老年代除了Serial Old(PS MarkSweep)收集器以外别无选择,直到Parallel Old收集器出世,

3.7、CMS收集器

特点

  1. 以获取最短回收停顿时间为目标
  2. 标记-清除
  3. 老年代收集器

优点:通过分步骤来降低停顿时间

  1. 初始标记(CMS initial mark) :初始标记仅仅标记一下GC Roots能直接关联到的对象,速度很快。
  2. 并发标记(CMS concurrent mark) :并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对 象图的过程
  3. 重新标记(CMS remark) :重 新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的 标记记录
  4. 并发清除(CMS concurrent sweep):并发清除阶段,清理删除掉标记阶段判断的已经死亡的 对象
  5. 其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

缺点

  1. CMS收集器,虽然将垃圾回收分成了几步,间接的来区分出来哪些需要停止用户线程,虽然这样做可以在某程度上不会导致用户线程停顿,却会占用了一部分线程(或者说处理器的计 算能力),而导致应用程序变慢,降低总吞吐量。
  2. 边清理边运行的垃圾,垃圾回收没办法及时清理,因此还需要空出来很大的内存来备用存放浮动垃圾。浮动垃圾的多少在于项目并发情况,假如那一瞬间访问特别多,那自然占用堆内存会很大,浮动垃圾空留空间是可以通过参数调的。
  3. 基于标记清除,就会有内存碎片问题。

可控制参数

  • -XX:+UseCMS-CompactAtFullCollection:开启标记整理,整理阶段是没办法运行用户线程的。
  • -XX:CMSFullGCsBefore- Compaction:该参数可以设置次数,也就是gc几次的时候,用一次标记整理,避免了非得到无法分配的情况才去整理,导致线程停顿。

3.8、G1收集器

特点

  1. JDK 9 G1宣告取代Parallel Scavenge加Parallel Old组合
  2. 目标是在延迟可控的情况下获得尽可能高的吞吐量
  3. 不再采用年轻代、老年代,而是把连续的Java堆划分为多个大小相等的独立区域(Region),那也就是说别的收集器都采用了分代,老年代和新生代各自有各自的收集器,所以必须要组合起来用,而G1被Oracle官方称为“全功能的垃圾收集器”,也就是只需要用这一个收集器。

优点

  1. G1收集器去跟踪各个Region里面的垃 圾堆积的“价值”大小,维护一 个优先级列表,根据用户设置的时间,优先处理回收价值收益最大的那些Region。说白了,就是你能接受的停顿时间大,收集器就能多释放点内存,假如你设置的小,他就只能少释放点,尽可能减少停顿时间,但是不可能控制的那么精准。

  2. 他跟CMS一样,采用了分步来完成收集:

    1. 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的
    2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,找出要回收的对象
    3. 最终标记(Final Marking):用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录(SATB记录我理解的就是回收和用户线程并行时候所产生的记录)。
    4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划
    5. 除了并发标记外,其余阶段也是要完全暂停用户线程的

缺点

  1. 使用记忆集来记录夸区域引用对象,使用记忆集避免全堆作为GC Roots扫描,每个Region都维护有自己的记忆集,Region数量多,故此G1至少要耗费大约相当于Java堆容量10%至20%的额 外内存来维持收集器工作。

可控制参数

  • -XX:G1HeapRegionSize:设置每个Region的大小
  • -XX:MaxGCPauseMillis:设定允许的收集停顿时间

猜你喜欢

转载自blog.csdn.net/weixin_43888891/article/details/124080810