前言
如果Java垃圾收集器(Java GC机制)与内存分配回收策略中提到的手机算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。今天我们要讨论的收集器的基于HotSpot虚拟机,其中所包含的收集器如图所示:
垃圾收集器
直到目前为止还没有最好的收集器出现,更没有万能的收集器,我们能选择的只是对应具体应用的收集器,接下来我们逐一介绍一下这些收集器。在介绍之前我们先了解一下并发收集和并行收集的区别以及吞吐量:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
- 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾收集时间)
新生代收集器
serial (复制算法)
Serial(串行)收集器是最基本、发展历史最悠久的收集器,它是采用复制算法的新生代收集器。它是一个单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,意味着它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止(“Stop The World”),这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接收的(想象一下你的电脑或者手机每运行一段时间就要暂停响应几分钟,这是什么感受)。 下图展示了Serial 收集器(Serial Old收集器)的运行过程:
parNew (复制算法)
ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,实际上两者共用了相当多的代码 ParNew/serial old收集器的工作过程如下图:
parallel scaverge(复制算法)
Parallel Scavenge收集器看上去和parNew一样——同样也是一个并行的多线程新生代收集器,使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput)。因此,parallel scaverge也被叫做”吞吐量优先“收集器,
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的MaxGCPauseMillis以及直接设置吞吐量大小的GCTimeRatio。还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节是它和ParNew收集器的一个重要区别。
小结:控制吞吐量和自适应调节。高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
老年代收集器
serial old(标记-整理)
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。 此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:
- 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
它的工作流程与Serial收集器相同:
parallel old(标记-整理)
parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old以外别无选择。由于老年代Serial Old收集器在服务端应用性能上的拖累,使用了parallel Scavenge收集器也未必能再整体应用上获得吞吐量最大化的效果。由于单线程的老年代收集集中无法充分利用服务器多CPU的处理能力,这种组合吞吐量甚至不如ParNew加CMS。直到Parallel Old诞生以后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。 Parallel Old收集器的工作流程与Parallel Scavenge相同:
cms(标记-清除)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合那些非常重视服务的响应速度的应用,例如互联网站或者B/S系统的服务端上的Java应用。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。 CMS收集器工作的整个流程分为以下4个步骤:
- 初始标记:仅标记一下GC Roots能直接关联到的对象;速度很快;但需要"Stop The World";
- 并发标记:进行GC Roots Tracing的过程;刚才产生的集合中标记出存活对象;应用程序也在运行;并不能保证可以标记出所有的存活对象;
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;需要"Stop The World",且停顿时间比初始标记稍长,但远比并发标记短;采用多线程并行执行来提升效率;
- 并发清除:回收所有的垃圾对象
小结:并发收集,低停顿。对cpu敏感,产生大量内存碎片。
G1收集器
G1(Garbage-First)收集器是一款面向服务端应用的垃圾收集器,具备以下特点:
- 并行与并发:能充分利用多CPU、多核环境下的硬件优势;可以使用多个CPU并行来缩短"Stop The World"停顿时间;也可以并发让垃圾收集与用户程序同时进行。
- 分代收集:能独立管理整个GC堆(新生代和老年代),而不需要与其他收集器搭配;能够采用不同方式处理不同时期的对象;虽然保留分代概念,但Java堆的内存布局有很大差别;将整个堆划分为多个大小相等的独立区域(Region);新生代和老年代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。
- 空间整合,不产生碎片:从整体看,是基于标记-整理算法;从局部(两个Region间)看,是基于复制算法;都不会产生内存碎片,有利于长时间运行,不会提前触发一次GC。
- 可预测的停顿:低停顿的同时实现高吞吐量;G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。
GC的分类
GC主要有YoungGC,FullGC(还有G1中独有的Mixed GC,收集整个young区以及部分Old区)
- YoungGC(Minor GC):新生代GC,指发生在新生代的垃圾收集动作,因为Java对象大多数具有朝生夕死的特性,所以Minor GC 很频繁,速度也比较快,尽管它会触发stop-the-world;
- FullGC(Major GC):指发生在老年代的GC,出现Major GC,经常会伴随至少一次的Minor GC(但不是绝对的)。发生频率低,且速度慢。
结语
| 名字 | 方式 | 分代 | 收集算法 | 目的 |
|---|---|---|---|---|
| Serial | 串行 | 新生代 | 复制 | 速度 |
| ParNew | 并行 | 新生代 | 复制 | 速度 |
| Parallel Scavenge | 并行 | 新生代 | 复制 | 吞吐量优先 |
| Serial Old | 串行 | 老年代 | 标记整理 | 速度 |
| Parallel Old | 并行 | 老年代 | 标记整理 | 吞吐量优先 |
| CMS | 并发 | 老年代 | 标记清除 | 速度 |
| G1 | 并发 | - | 标记整理加复制 | 速度 |