HotSpot的经典垃圾收集器

前言

垃圾算法是内存回收的方法论,垃圾收集器就是内存回收的实践者。
在开始之前,如果对垃圾回收算法还不了解的同学可以先去看一下这篇文章垃圾回收算法

下图是HotSpot虚拟机中的垃圾收集器,若两个收集器之间存在连线,这说明它们可以搭配使用,下面我们将一一学习。
HotSpot虚拟机的垃圾收集器


Serial收集器

Serial收集器是最基础最早出现的收集器,它是一个单线程的收集器,在进行垃圾收集时,必须暂停其他所有线程,直到收集结束(Stop The World)。

Serial/Serial Old收集器运行示意图
虽然Serial收集器会暂停其他所有工作线程,但是它仍然是HotSpot虚拟机运行在客户端模式下默认新生代收集器,它有着优于其它收集器的地方,那就是简单高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器中额外内存消耗(Memory Footprint)最小的;对于单核处理器或处理器核心数少的环境来说,Serial收集器由于没有线程交互的开销,可以获得最高的单线程收集效率。

ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了使用多线程进行垃圾收集,还包括Serial收集器可用的控制参数(如-XX: SurvivorRatio,-XX: PretenureSizeThreshold等)、收集算法、暂停用户线程、对象分配、回收策略等都和Serial收集器一致。

运行示意图

并发收集与并行收集的概念:

  • 并行(Parallel):并行描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,默认此时用来护线程是处于等待状态。
  • 并发(Concurrent):并发描述的是垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行,但由于垃圾收集器线程占用了一部分系统资源,所以此时应用程序的处理吞吐量将受到一定影响。

Parallel Scavenge

Parallel Scavenge收集器也是一款新生代收集器,也是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。
Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput),吞吐量是指运行用户代码时间:(运行用户代码时间+运行垃圾收集时间)的比值。

Parallel Scavenge/Parallel Old收集器运行示意图

Parallel Scavenge配置参数:

  • -XX: MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,即收集器尽力保证内存回收花费的时间不超过用户设定值。不过垃圾收集停顿时间的缩短是以牺牲新生代空间为代价换取的:系统将新生代的内存调小一些,垃圾收集器收集小一点的空间肯定比大的块,但是这也直接导致垃圾收集发生的频率提高,随存停顿时间下降,但是吞吐量也降下来了。
  • -XX: GCTimeRatio参数的取值范围1-99的整数,也就是垃圾手机时间占总时间的比率,相当于吞吐量的倒数。例如将参数设置为19,1/(1+19)=0.05,即允许最大垃圾收集时间占总时间的5%;默认值为99,1/(1+99)=0.01,即允许最大1%的垃圾收集时间。

Parallel Scavenge收集器区别于ParNew收集器的特性:

除了开头所说的与ParNew、Serial收集的相同点之外,Parallel Scavenge收集器也称作“吞吐量优先收集器”,Parallel Scavenge收集还有一个参数-XX: +UseAdaptiveSizePolicy(使用自适应大小策略),当开启这个配置,则不需要手动指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX: SurvivorRatio),对象老年代大小(-XX: PretenureSizeThreshold)等参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最适合的停顿时间或最大的吞吐量。
在使用-XX: +UseAdaptiveSizePolicy的过程中,我们只需通过-Xmx设置最大堆,通过-XX: MaxGCPauseMillis-XX: GCTimeRatio设立优化目标,剩下的参数细节调节工作就可以交给虚拟机完成了。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同样是单线程收集器。Serial Old的主要意义也是在客户端模式下的HotSpot虚拟机使用。

JDK 8时将Serial+CMS、ParNew+Serial Old声明为废弃,JDK 9时完全取消了这些组合。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,支持并发收集,基于标记整理算法实现,从JDK 1.6开始提供的。这两个的搭配也是JDK1.7、JDK1.8的默认垃圾收集器。

在JDK 1.6之前,老年代除了和Serial Old其他的别无选择,由于Serial Old是单线程的收集器,无法充分利用多处理器的并行处理能力,收集时需要暂停所有工作线程,使用Parallel Scavenge收集器也无法再整体上获得吞吐量最大化的效果。

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种获取最短回收停顿时间为目标的收集器。

运作过程:

  1. 初始标记:标记GC Root能关联到的对象,标记过程中需要暂停用户线程。
  2. 并发标记:从GC Root的直接关联对象开始遍历整个对象图进行标记,并发运行。
  3. 重新标记:修正并发标记期间,因并发标记期间,用户线程继续运作而导致标记产生变动的那部分对象的标记记录。
  4. 并发清除:清理删除掉标记阶段判断已经死亡的对象,并发运行。

CMS收集器工作示意图
CMS虽然有并发收集、低停顿等优点,但是仍然存在三个明显的缺点:

  1. CMS并发标记与并发清除这两步时,虽然不会导致用户线程停顿,但是却会占用一部分线程,而导致应用变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4个,但是当处理器数量降低的情况下,CMS占用的处理器资源就会升高。
  2. CMS无法处理浮动垃圾(Floating Garbage),在CMS的并发标记与并发清理阶段时,用户线程在运行的过程中伴随着新的垃圾对象不断产生,这个时间段产生的垃圾对象称为浮动垃圾,这些垃圾要等到下一次垃圾收集时才能清理掉。同样CMS在并发收集过程中需要预留空间提供给用户线程使用,如果CMS运行期间预留的内存不足程序分配对象,就会出现一次并发失败(Concurrent Mode Failure),这时虚拟机将冻结用户线程执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集。
  3. CMS是基于标记-清除算法实现的收集器,也意味着,在垃圾收集结束之后会产生大量的空间碎片,当空间碎片过多时,分配大对象会因为无法找到足够大的连续空间,而不得不触发Full GC。

Garbage First收集器

Garbage First简称G1,主要面向服务端应用的垃圾收集器,JDK 9发布时,G1取代了JDK 8中默认的收集器组合Parallel Scavenge和Parallel Old。《G1: One Garbage Collector To Rule Them All》

G1与其他垃圾收集器的区别:
在G1出现之前所有的垃圾收集器,收集的主要目标范围:新生代(Minor GC),老年代(Major GC),Java堆(Full GC)。而G1的Mixed GC模式可以面向堆内存任意部分来组成回收集(Collection Set)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收利益最大。

G1的特点:
G1也仍是遵守分代收集理论设计的,但G1不再坚持固定大小及固定数量的分代区域划分,而是把连续的Java堆分为多个大小相等的独立区域(Region),每个Region都可以根据需要,扮演新生代的Eden、Survivor、老年代空间。
G1之所以能够预测停顿时间模型,是因为它将Region作为单次回收的最小单元,可以有计划地避免在整个Java堆中进行全区域的垃圾收集。
Region存在一类特殊的Humongous Region,专门用来存储大对象,大对象的定义为:跨度超过一半区域大小的对象被认为是“超大对象”。每个Region的大小可以通过参数-XX: G1HeapRegionSize设定,取值范围1MB-32MB,且为2的N次幂,这些大对象将被存放在N个连续的Humongous Region中,Eden与Survivor仍然是年轻代,超大对象会被直接分配到老年代中,Region分区示意图如下。

Region分区示意图


优势:
与其他垃圾收集器的优势

  • G1的新生代和老年代不再是固定的,他们都是一系列区域的动态集合。
  • 它将Region作为单次回收的最小单元,即回收到的内存空间都是Region大小的整数倍,可以有计划的避免在Java堆中进行全区域的垃圾收集。
  • G1收集器跟踪各个Region里面的垃圾堆积“价值”(回收所获得的空间大小及回收所需要的的时间)的大小,然后后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间(-XX: MaxGCPauseMillis来设定允许停顿时间,默认200ms),优先处理回收价值最大的那些Region,因此叫做“Garbage First”,这样就保证G1收集器在有限时间内获取尽可能高的收集效率及收益。

疑问:
Java堆分成多个独立的Region后,跨Region引用对象如何解决?

  • G1收集器使用记忆集(不了解的可以先去看看这篇)避免全堆作为GC Roots扫描,和其他收集器不同的是,G1收集器每个Region都有自己的记忆集,这些记忆集会记录下别的Region指向自己的指针(也记录自己指向别的Region的指针),并标记这些指针分别在哪些卡页的范围内。由于Region数量比传统收集器的分代数量多,因此G1收集器所需要的内存空间也更高。

如何在并发标记阶段保证互不干扰地运行以及CMS与G1处理有什么差别?

  • 用户线程与垃圾收集器并发工作将会导致其中一个最严重的后果:对象消失,CMS采用增量更新算法实现,而G1收集器则是通过原始快照(SATB)算法实现的。
  • G1为每个Region设计了两个名为TAMS(Top at Mark Start)的指针,把Region中的一部分空间划分出来用于并发回收过程中新的对象分配,且新分配的对象地址必须在这两个指针的位置以上,G1收集器默认在这个地址以上的对象是被隐式标记过得,即默认对象存活。与CMS中的并发失败会导致Full GC一样,若G1内存回收速度赶不上内存分配的速度,G1收集器也将暂停用户线程,导致Full GC而长时间暂停用户线程。

G1收集器怎么满足用户期望呢?
用户可以通过-XX: MaxGCPauseMillis参数指定停顿时间,默认值为200ms,如果设置过小很有可能会导致垃圾堆积,触发Full GC反而降低性能。

G1收集的运作过程:

  1. 初始标记(Initial Marking):标记GC Roots能直接关联到的对象,修改TAMS指针的值,让下阶段用户线程并发运行时,能正确地在可用的Region中分配新的对象。过程中暂停用户线程,耗时短(在Minor GC时同步完成)。
  2. 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆中的对象图,找出要回收的对象,这阶段耗时较长,但并发执行。当对象图扫描完成以后,需要重新处理SATB记录下的在并发时有引用变动的对象。
  3. 最终标记(Final Marking):短暂暂停用户线程,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。
  4. 筛选回收(Live Data Counting and Evacuation):负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户期望的停顿时间制定回收计划,可自由选择任意多的Region构成回收集,然后决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这一步涉及存活对象的移动,必须暂停用户线程,由多条收集器线程并行完成。

G1收集器运行示意图
CMS与G1的异同:

  • G1和CMS都是用卡表来处理跨代指针。G1相比来说更为复杂,因为堆中每个Region无论是新生代还是老年代,都各自维护一份卡表,这也将导致G1的记忆集将占用Java堆更多的内存空间;CMS就简单很多,只有唯一一份,只用处理老年代到新生代的引用,反之则不需要。
  • CMS是基于标记-清除算法实现的;G1整体上是基于标记-整理算法实现的收集器,但局部(两个Region之间)上看又是基于标记-复制算法实现,这也将决定着G1垃圾收集期间不会产生内存空间碎片(可以避免程序在分配大对象时,因找不到连续的内存空间而提前触发下一次的收集)。
  • CMS与G1都是用写后屏障来更新维护卡表;G1为了实现SATB算法,还需要使用写前屏障来追踪并发时指针变化情况。相比CMS的增量更新算法,SATB能减少并发标记和重新标记阶段的消耗,避免在最终标记阶段停顿时间过长。

垃圾收集日志

JDK 9时把HotSpot所有功能的日志都归到了-Xlog参数上。

具体使用方式可以通过java -Xlog:help查询具体配置方法:

C:\Users\hp>java -version
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)

C:\Users\hp>java -Xlog:help
-Xlog Usage: -Xlog[:[what][:[output][:[decorators][:output-options]]]]
         where 'what' is a combination of tags and levels of the form tag1[+tag2...][*][=level][,...]
         Unless wildcard (*) is specified, only log messages tagged with exactly the tags specified will be matched.

Available log levels:
 off, trace, debug, info, warning, error

Available log decorators:
 time (t), utctime (utc), uptime (u), timemillis (tm), uptimemillis (um), timenanos (tn), uptimenanos (un), hostname (hn), pid (p), tid (ti), level (l), tags (tg)
 Decorators can also be specified as 'none' for no decoration.

Available log tags:
 add, age, alloc, aot, annotation, arguments, attach, barrier, biasedlocking, blocks, bot, breakpoint, census, class, classhisto, cleanup, compaction, constraints, constantpool, coops, cpu, cset, data, defaultmethods, dump, ergo, exceptions, exit, fingerprint, freelist, gc, hashtables, heap, humongous, ihop, iklass, init, itables, jni, jvmti, liveness, load, loader, logging, mark, marking, methodcomparator, metadata, metaspace, mmu, module, monitorinflation, monitormismatch, nmethod, normalize, objecttagging, obsolete, oopmap, os, pagesize, patch, path, phases, plab, promotion, preorder, protectiondomain, ref, redefine, refine, region, remset, purge, resolve, safepoint, scavenge, scrub, stacktrace, stackwalk, start, startuptime, state, stats, stringdedup, stringtable, stackmap, subclass, survivor, sweep, task, thread, tlab, time, timer, update, unload, verification, verify, vmoperation, vtables, workgang, jfr, system, parser, bytecode, setting, event
 Specifying 'all' instead of a tag combination matches all tag combinations.

Described tag combinations:
 logging: Logging for the log framework itself

Available log outputs:
 stdout, stderr, file=<filename>
 Specifying %p and/or %t in the filename will expand to the JVM's PID and startup timestamp, respectively.

Some examples:
 -Xlog
         Log all messages using 'info' level to stdout with 'uptime', 'levels' and 'tags' decorations.
         (Equivalent to -Xlog:all=info:stdout:uptime,levels,tags).

 -Xlog:gc
         Log messages tagged with 'gc' tag using 'info' level to stdout, with default decorations.

 -Xlog:gc,safepoint
         Log messages tagged either with 'gc' or 'safepoint' tags, both using 'info' level, to stdout, with default decorations.
         (Messages tagged with both 'gc' and 'safepoint' will not be logged.)

 -Xlog:gc+ref=debug
         Log messages tagged with both 'gc' and 'ref' tags, using 'debug' level, to stdout, with default decorations.
         (Messages tagged only with one of the two tags will not be logged.)

 -Xlog:gc=debug:file=gc.txt:none
         Log messages tagged with 'gc' tag using 'debug' level to file 'gc.txt' with no decorations.

 -Xlog:gc=trace:file=gctrace.txt:uptimemillis,pids:filecount=5,filesize=1m
         Log messages tagged with 'gc' tag using 'trace' level to a rotating fileset of 5 files of size 1MB,
         using the base name 'gctrace.txt', with 'uptimemillis' and 'pid' decorations.

 -Xlog:gc::uptime,tid
         Log messages tagged with 'gc' tag using 'info' level to output 'stdout', using 'uptime' and 'tid' decorations.

 -Xlog:gc*=info,safepoint*=off
         Log messages tagged with at least 'gc' using 'info' level, but turn off logging of messages tagged with 'safepoint'.
         (Messages tagged with both 'gc' and 'safepoint' will not be logged.)

 -Xlog:disable -Xlog:safepoint=trace:safepointtrace.txt
         Turn off all logging, including warnings and errors,
         and then enable messages tagged with 'safepoint' using 'trace' level to file 'safepointtrace.txt'.

参考文献

《深入理Java虚拟机》周志明

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人生逆旅我亦行人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值