Jvm的经典垃圾收集器介绍
如果说垃圾收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。经典垃圾收集器虽然不是最先进的技术,但仍然值得我们去学习,也才能更好的与最新的收集器去对比!
Serial(串行)收集器
Serial收集器是最基础、历史最悠久的收集器。这个收集器是一个单线程收集器,只使用一个处理器或者一条收集线程去完成垃圾收集工作,而且在进行垃圾收集的时候必须暂停其他所有工作线程,直到收集结束,这个暂停其他所有线程我们称为“Stop The World”(后面都简称STW来介绍)。
收集器运行示意图如下:
STW是由虚拟机在后台自动发起和自动完成的,但是在由于停掉了其他所有的线程,对很多应用是不能接受的,也会影响用户的体验,所以我们后来开发的收集器很多都是为了降低这个停顿的时间。
Serial收集器虽然是最早出现的,但是并不代表它已经被我们淘汰了。它也有着自己独特的优点:
与其他收集器的单线程相比,就是简单而高效。
- 对于内存受限的环境它是所有收集器中额外内存消耗最小的;
- 对于处理器核心较少的环境,Serial 由于没有线程交互开销,可获得最高的单线程收集效率。
因为这些优点,Serial收集器对于运行在客户端模式下的虚拟机是一个很好的选择,而且它也是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。
ParNew收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,除了使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。
收集器运行示意图如下:
ParNew 是虚拟机在激活CMS(下面会介绍的收集器)后默认的新生代收集器,一个重要原因是除了 Serial 外只有它能与 CMS 配合。所以它也是JDK7之前的遗留系统中首选的新生代收集器!
可以说直到CMS的出现才巩固了ParNew的地位,但成也萧何败也萧何,随着垃圾收集器技术的不断改进,更先进的G1收集器带着CMS继承者和替代者的光环登场。ParNew淡出了视线...
Parallel Scavenge收集器
Parallel(并行)Scavenge也是一款新生代收集器,基于标记-复制算法实现的收集器,也是能够并行的多线程收集器。
Parallel Scavenge收集器与其他多线程收集器关注点不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间。而Parallel Scavenge的目标则是要达到 一个可控制的吞吐量。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)
如果虚拟机完成某个任务,用户代码加上垃圾收集总共耗费了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,不过在老年代采用的是标记-整理算法。这个收集器主要意义也是供客户端模式下的HotSpot虚拟机使用。
收集器运行示意图:
它的用途主要是有两个:
- 在JDK5以及之前的版本与Parallel Scavenge收集器搭配使用
- 作为CMS收集器发生失败时的后备预案
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。有了这个老年代收集器之后,我们的Parallel Scavenge也就有了除Serial Old以外的其他老年代收集器的搭配了。
这二者搭配之后,“吞吐量优先”收集器终于有了比较名副其实的搭配组合。
二者搭配收集器示意图如下:
我们的JDK7,JDK8默认使用的垃圾收集器就是这个搭配组合!也是我们注重吞吐量的优先选择。
CMS收集器
上面介绍了到了关注吞吐量的垃圾收集器,下面又回到了介绍我们以获取最短回收停顿时间为目标的收集器——CMS收集器。CMS收集器是基于标记-清除算法实现的,过程相对复杂,可以分为下面四个流程:
- 初始标记
- 并发标记
- 重新标记
- 并发清除
其中初始标记、重新标记这个两个步骤仍然需要STW。初始标记仅是标记 GC Roots 能直接关联的对象,速度很快。并发标记从 GC Roots 的直接关联对象开始遍历整个对象图,耗时较长但不需要停顿用户线程。重新标记则是为了修正并发标记期间因用户程序运作而导致标记产生变动的那部分记录。并发清除清理标记阶段判断的已死亡对象,不需要移动存活对象,该阶段也可与用户线程并发。
收集器运行示意图:
整个过程中耗时比较长的并发标记和并发清除都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点
虽然CMS是一款优秀的收集器,也是HotSpot虚拟机追求低停顿的第一次成功尝试,但是还是有三个比较明显的缺点
- 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量。
- 无法处理浮动垃圾,有可能出现并发失败而导致 Full GC。(在CMS的并发标记和并发清理阶段,用户线程是还在继续运行的,程序在运行自然就还会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集
时再清理掉。这一部分垃圾就称为“浮动垃圾”。) - 基于标记-清除算法,产生空间碎片。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC的情况。
Garbage First收集器
Garbage First收集器(简称G1)开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。其主要目的是用来替换CMS收集器!它是一款面向服务端应用的垃圾收集器,在JDK9,宣告取代了之前默认使用的Parallel Scavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。
在G1收集器出现之前的所有其他收集器,包括CMS在内,垃圾收集的目标范围要么是整个新生代(Minor GC),要么就是整个老年代(Major GC),再要么就是整个Java堆(Full GC)。而G1跳出了这个樊笼,它可以面向堆内存任何部分来组成回收集(Collection Set,一般简称CSet)进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式。
我们的G1收集器将堆划分成多个大小相等的独立Region区域,每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。然后跟踪各 Region 里垃圾的价值,价值即回收所获空间大小以及回收所需时间的经验值,在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间优先处理回收价值最大的 Region。这种方式保证了 G1 在有限时间内获取尽可能高的收集效率。
G1在不考虑用户线程运行过程中的动作,运作过程大致可划分为以下四个步骤:
-
初始标记:标记 GC Roots 能直接关联到的对象,让下一阶段用户线程并发运行时能正确地在可用 Region 中分配新对象。需要 STW 但耗时很短,在 Minor GC 时同步完成。
-
并发标记:从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理 SATB 记录的在并发时有变动的对象。
-
最终标记:对用户线程做短暂暂停,处理并发阶段结束后仍遗留下来的少量 SATB 记录。
-
筛选回收:对各 Region 的回收价值排序,根据用户期望停顿时间制定回收计划。必须暂停用户线程,由多条收集线程并行完成。
运行示意图如下:
可以看到除了并发标记,其他阶段也是要求STW的。它并非一味追求低延迟,而是在延迟可控的情况下获得尽可能高的吞吐量!而且可由用户指定期望停顿时间虽然是 G1 的一个强大功能,但该值不能设得太低,一般设置为100~300 ms。不然为了达到我们设置的这个低延迟的收集效果,我们每次的收集区域很小,渐渐的就赶不上了分配内存的速度了,最后因为垃圾变多而导致Full GC得不偿失!
总结
关于后面两款垃圾收集器介绍的并不是特别完整,只是大概了解。具体的细节实现特点并未深入,需要深入的可以去看下面的参考资料。
参考资料
深入理解Java虚拟机《第三版》