Serial回收器:串行回收
Serial收集器是最基础、历史最悠久的收集器,曾经(在JDK1.3.1之前)是HotSpot虚拟机新生代收集器的唯一选择。这个收集器是一个单线程工作的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
采用复制算法、串行回收和stop-the-world机制的方式执行内存回收
Serial收集器还提供用于执行老年代垃圾收集的Serial Old收集器。也是采用串行回收和stop the world机制,只不过内存回收算法使用的是标记-压缩算法。
Serial Old是运行在Client模式下默认的老年代的垃圾回收器
Serial Old在server模式下主要有两个用途:1.与新生代的Parallel Scavenge配合使用;2.作为老年代CMS收集器的后备垃圾收集方案
优势:简单高效,对于单个CPU的环境来说,Serial收集器由于没有线程交互开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其它所有的工作线程,直到它收集结束。
1 | -XX:PrintCommandLineFlags# 查看使用的垃圾收集器 |
ParNew回收器:并行回收
ParNew收集器则是Serial收集器的多线程版本。除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也公用了相当多的代码。
ParNew收集器除了采用并行回收的方式执行内存回收外,两款垃圾收集器之间几乎没有任何区别。ParNew收集器在年轻代中同样也是采用复制算法,stop-the-world机制。
对于新生代,回收次数频繁,使用并行方式高效;对于老年代,回收次数少,使用串行方式节省资源。
ParNew收集器运行在多CPU的环境下,由于可以充分利用多CPU、多核心等物理硬件资源优势,可以更快速地完成垃圾收集、提升程序的吞吐量。
ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程(Hyper-Threading)技术实现的伪双核处理器环境中都不能百分百保证超越Serial收集器。当然,随着可以被使用的处理器核心数量的增加,PurNew对于垃圾收集时系统资源的高效利用还是很有好处的。它默认开启的收集线程数与处理器核心数量相同,在处理器核心非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
Parnew GC只能与CMS收集器配合工作
1 | -XX:+UseParNewGC #手动指定收集器(只表示年轻代) |
Parallel Scavenge回收器:吞吐量优先
Parallel Scavenge收集器它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗时间的比值:
Parallel Scavenge收集器提供了两个参数以及用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
-XX:MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽力保证内存回收花费的时间不超过用户设定值。不过如果把这个参数的值设置地更小一点就能使得系统的垃圾收集速度变得更快,垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的:系统把新生代调的小一些,收集300MB新生代肯定比收集500MB快,但这也直接导致垃圾收集发生得更频繁,原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次、每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。
-XX:GCTimeRatio参数的值则应当是一个大于0小于100的整数,也就是垃圾收集时间占总时间的比率,相当于吞吐量的倒数。譬如把此参数设置为19,那允许的最大收集时间就占总时间的5%(即1/(1+19)),默认值为99,即允许最大1%的垃圾收集时间。
由于与吞吐量关系密切,Parallel Scavenge收集器经常被称为“吞吐量优先收集器”。除了上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得我们关注。这是一个开关参数,当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。这种调节方式称为垃圾收集的自适应的调节策略(GC Ergonomisc)。自适应调节策略是Parallel和ParNew一个重要区别。
与ParNew收集器不同,Parallel回收器的目标则是达到一个可控制的吞吐量,它也被称为吞吐量优先的垃圾收集器。
高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。因此,常见在服务器环境中使用。
Parallel old收集器,用来代替老年代的Serial old收集器。
Parallel old收集器采用了标记压缩算法,但同样也是基于并行回收和stop-the-world机制。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程的收集器,使用标记-整理算法。这个收集器的主要意义也是提供客户端模式下的HotSpot虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在JDK5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。这个收集器是直到JDK6时才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于相当尴尬的状态,原因是如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器以外别无选择,其他表现良好的老年代收集器,如CMS无法与它配合工作。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整体上获得吞吐量最大化的效果。同样,由于单线程的老年代收集中无法充分利用服务器多处理器的并行处理能力,在老年代内存空间很大而且硬件规格比较高级的运行环境中,这种组合的总吞吐量甚至不一定比ParNew加CMS的组合来的优秀。
直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合,在注重吞吐量或者处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。
它实现了让垃圾收集线程与用户线程同时工作。
CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。停顿时间越短就越适合与用户交互的程序,良好的响应速度能提升用户体验
CMS的垃圾收集算法采用标记-清除算法,并且也会stop-the-world
- 初始标记
所有的工作线程都会stop-the-world机制而短暂的暂停,这个阶段的主要任务仅仅只是标记出GC ROOTs能直接关联到的对象,速度很快。
- 并发标记
从GC roots的直接对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记
在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。需要Stop The World。
- 并发清除
在此阶段清理删除标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,这个阶段也是可以与用户线程同时并发的。
在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器是HotSpot虚拟机追求低停顿的第一次成功尝试,但是它还远达不到完美的程度,至少有以下三个明显的缺点:
- CMS收集器对处理器资源非常敏感。事实上,面向并发程序设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。CMS默认启动的回收线程数是(处理器核心数量+3)/4,也就是说,如果处理器核心数在四个或以上,并发回收时垃圾收集线程只占用不超过25%的处理器,并且会随着处理器核心数量的增加而下降。但是当处理器核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集线程,就可能导致用户程序的执行速度忽然大幅度降低。为了缓解这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种,所做的事情和以前单核处理器年代PC机操作系统靠抢占式多任务来模拟多核并行多任务的思想一样,是在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对于用户程序的影响就会显得较少一些,直观感受是速度变慢的时间更多了,但速度下降幅度就没有那么明显。实践证明增量式CMS收集器效果很一般。
- 由于CMS收集器无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“stop the world”的Full GC的产生。在CMS的并发标记和并发清理阶段,用户线程还是在继续运行的,程序 在运行自然就还会伴随着有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当此收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为“浮动垃圾”。同样也是由于在垃圾收集阶段用户线程还需要持续运行,那就还需要预留足够内存空间提供给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代几乎完全被填满了再进行收集,必须预留一部分空间供并发收集时的程序运作使用。在JDK5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在实际应用中老年代增长并不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraciton的值来提高CMS的触发百分比,降低内存回收频率,获得更好的性能。到了JDK6时,CMS收集器的启动阈值已经默认提升至92%。但这又会更容易面临另一种风险:要是CMS收集器的启动预留的内存无法满足程序分配新对象的需要,就会出现一次“并发失败”,这时候虚拟机将不得不启动后备预案:冻结用户线程的执行,临时启用Serial Old收集器来重新进行老年代的垃圾收集,但这样停顿时间就很长了。所以参数-XX:CMSInitiatingOccupancyFraciton设置得太高很容易导致大量得并发失败产生,性能反而降低,用户在生产环境中根据实际应用情况来权衡设置。
- 还有一个缺点,CMS是一款基于“标记-清除”算法实现得收集器。意味着收集结束时有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发Full GC的情况。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认是开启的,次参数从JDK9开始废弃),用于在CMS收集器不得不进行FullGC时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,(在Shenandoah和ZGC出现前)是无法并发的。这样空间碎片问题是解决了,但停顿时间又会变长,因此虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction(此参数从JDK9开始废弃),这个参数的作用是要求CMS收集器在执行过若干次(数量由参数值决定)不整理空间的FullGC之后,下一次进入FullGC前会先进性碎片整理(默认值为0,表示每次进入FullGC时都进行碎片整理)。
CMS收集器的垃圾回收算法只是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么CMS在为新对象分配内存空间时,将无法使用指针碰撞技术,而只能够选择空闲列表执行内存分配。
CMS的优点:
- 并发收集
- 低延迟
CMS的弊端:
- 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC
- CMS收集器对CPU资源非常敏感。它虽然不会导致停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
- CMS收集器无法处理浮动垃圾。在并发标记阶段中产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终导致这些新产生的垃圾对象没有被及时回收。
G1收集器:区域化分代式
并行与并发
并行性:G1在回收期间,可以有多个GC线程同时工作,有利于多核计算能力。此时用户线程STW
并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此,一般来说,不会在整个回收阶段发生完全阻塞应用程序的情况。
分代收集
G1依然属于分代型垃圾收集器,它会区分年轻代和老年代,年轻代依然有伊甸园区和幸存者区。但从堆的结构上看,不要求整个Eden区、年轻代或者老年代都是连续的,也不再坚持固定大小和固定数量
将堆空间分为若干个区域,这些区域中包含了逻辑上的年轻代和老年代
和之前的各类回收器不同,它同时兼顾年轻代和老年代。对比其他回收器,或者工作在年轻代、或者工作在老年代
空间整合
CMS:标记清除算法、内存碎片、若干次GC后进行一次碎片整理
G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间,Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获得很好的收集效果。
Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数:-XX:G1HeapRegionSize设定,取值范围为1MB~32MB,且应为2的N次幂。而对于某些超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待。
G1将内存划分为一个个的region。内存的回收以region作为基本单位的。region之间是复制算法,但整体上实际可看作是标记-压缩算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当Java堆非常大的时候,G1的优势更加明显。
G1收集器的运作大致可划分为以下几个步骤:
- 初始标记:仅仅是标记以下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
- 并发标记:从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里面的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
G1收集器具备如下特点:
空间整合
整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
可预测的停顿时间模型
G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
用户通过-XX:MaxGCPauseMillis参数指定的停顿时间只意味着垃圾收集之前的期望值。G1收集器的停顿预测模型是以衰减均值为理论基础来实现的,在垃圾收集过程中,G1收集器会记录每个Region回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得出平均值、标准偏差、置信度等统计信息。换句话说,Region的统计状态越新越能够决定其回收的价值。然后通过这些信息预测现在开始回收的话,由哪些Region组成会收集才可以在不超过期望停顿时间的约束下获得最高的收益。
垃圾回收器的分类
- 按线程数分:
- 串行垃圾回收器
- 并行垃圾回收器
- 按工作模式分
- 并发式垃圾回收器
- 独占式垃圾回收器
- 碎片处理方式分:
- 压缩式垃圾回收器
- 非压缩式垃圾回收器
- 按工作的内存区间分:
- 年轻代垃圾回收器
- 老年代垃圾回收器