本文主要介绍HotSpot虚拟机中常见的七款垃圾收集器,如下:
1. Serial收集器
是最古老的新生代垃圾收集器。其特点是单线程执行,且在工作时,需要暂停其它所有的线程。
它的优势是简单且高效,额外内存消耗小。
目前是HotSpot虚拟机运行在客户端模式下的默认新生代垃圾收集器。
2. ParNew收集器
可以看作是Serial的多线程并行版本。
目前ParNew和Serial是唯二可以和CMS配合使用的新生代收集器。且在使用-XX: +UseConcMarkSweepGC参数开启CMS垃圾收集器时,默认就选择了ParNew作为新生代的垃圾收集器。
3. Parallel Scavenge 收集器
是一款关注**吞吐量(Throughput)**的新生代垃圾收集器。
吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 运行垃圾收集时间 )
Parallel Scavenge提供了两个参数用于精确控制吞吐量,分别为:
-XX: MaxGCPauseMillis:最大垃圾收集停顿时间,是一个大于0的毫秒数。收集器将尽可能保证回收花费的时间不超过用户的设定值;-XX: GCTimeRatio:吞吐量的大小。允许设置(0, 100)范围内的整数。默认值为99,表示最大允许1 / ( 1 + 99 ) = 1%的垃圾收集时间;
此外,Parallel Scavenge还提供了一个参数XX: +UseAdaptiveSizePolicy用于开启自适应调节策略。在开启该策略之后,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整新生代大小(-Xmn)、Eden区和Survivor区的比例(-XX:SurvivorRatio)等参数,以提供最合适的停顿时间或者最大的吞吐量。在手工优化存在困难的情况下,使用自适应调节策略是个不错的选择。
4. Serial Old收集器
是Serials收集器的老年代版本。其也是HotSpot虚拟机运行在客户端模式下的默认老年代垃圾收集器。在服务端模式下,其一般与Parallel Scavenge 收集器配合使用,或作为CMS收集器发生失败后的预备方案。
5. Parallel Old收集器
是Parallel Scavenge收集器的老年代版本。
在关注吞吐量的场景下,可以优先考虑Parallel Scavenge + Parallel Old的组合。同时,这个组合也是Java8默认所选择的。
# 执行以下命令
java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=268435456
-XX:MaxHeapSize=4294967296
-XX:+PrintCommandLineFlags
-XX:+UseCompressedClassPointers
-XX:+UseCompressedOops
# UseParallelGC表示使用Parallel Scavenge + Parallel Old 组合
-XX:+UseParallelGC
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)
6. CMS 收集器
全称Concurrent Mark Sweep,是一种以获取最短回收停顿时间为目标的收集器,基于"标记-清除"算法实现。
其工作过程较为复杂,分为以下四个步骤:
- 初始标记:标记
GC Root能够直接关联到的对象,速度很快,但需要Stop The World, STW; - 并发标记:从
GC Root的直接关联对象开始遍历整个内存,虽然过程时间较长,但并不需要暂停用户线程; - 重新标记:用于修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,仍然需要
STW; - 并发清除:清理删除掉标记阶段判断的已经死亡的对象,可以和用户线程并发执行;
CMS虽然是一款优秀的"低停顿"垃圾收集器,不过也有几个缺点:
-
并发阶段虽然不会停止用户线程的执行,但仍然会占用系统资源,导致应用运行效率降低;
-
垃圾收集线程与用户线程并发执行,也就意味着需要给用户线程预留一部分资源。JDK6之后,默认的
CMS启动阈值为92%,也就是说,给用户线程留有8%的内存资源。当这部分内存资源不足时,就会出现"并发失败(Concurrent Mode Failure, CMF) "。CMS提供了一个后备机制来处理CMF:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集。不过这也会导致较长的停顿时间; -
CMS是一款基于"标记-清除"算法的垃圾收集器,会产生大量的内存碎片。在给大对象分配内存空间时,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。虽然CMS提供了两个优化参数,但实际效果并不理想:-XX: +UseCMSCompactAtFullCollection:默认开启,用于在CMS收集器不得不进行Full GC时开启内存碎片的合并整理过程。这个过程必须STW,造成较长的停顿时间;-XX:CMSFullGCsBeforeCompaction:要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的Full GC之后,下一次进入Full GC前会先进行碎片整理。默认值为0,表示每次进入Full GC时都进行碎片整理;
7. G1收集器
全称Garbage First。其设计目标是在延迟可控的情况下获得尽可能高的吞吐量。
其使用Mixed GC的模式,可以面向堆内存的任何部分来组成"回收集(Collection Set, CSet)"进行回收。衡量标准不再是该内存区域属于哪个分代,而是哪块内存中存放垃圾最多,回收收益最大。
G1将Java堆划分为多个大小相等的独立区域,称为Region。每个Region都已根据需要,扮演Eden 、Survivor 、或老年代空间。同时,Region也是单次回收的最小单位。G1会跟踪每个Region的垃圾"价值"大小,在后台维护一个优先级列表,并根据用户所设定的收集停顿时间(-XX: MaxGCPauseMillis,默认200毫秒)优先回收价值最大的Region。
有一部分特殊的Region被识别为Humongous区域,专门用来存储大对象(G1将大小超过Region容量一半的对象识别为大对象)。超过了整个Region容量的超级大对象,会被存放在多个连续的Humongous Region中。G1在大多数情况下,都将Humongous Region作为老年代的一部分看待。
G1收集器的运作过程大致可划分为以下四个步骤:
-
初始标记:仅仅只是标记一下
GC Roots能直接关联到的对象,并且修改TAMS指针的值。这个阶段需要停顿用户线程,但耗时很短;G1会为每个Region分配两个被称为TAMS的指针,发回收时新分配的对象地址都必须要在这两个指针之间。 -
并发标记:从
GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。 -
最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的记录。
-
筛选回收:负责更新
Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。需要暂停用户线程。