【JVM】Java 中的经典垃圾回收器
从不同角度分析垃圾收集器,可以将其划分为不同的模型。
按线程数分,可以分为串行垃圾回收器和并行垃圾回收器;按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器;按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器;而按工作的内存区间,又可分为新生代垃圾回收器和老年代垃圾回收器。本文就基于工作的内存区间划分,来介绍七种经典的垃圾回收器,下图是它们的工作区间以及搭配方式。
Young generation
Serial 收集器
看名字就能猜到,这个收集器是一个单线程工作的收集器,但是它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它工作结束。同时,其采用的是标记复制算法。
Serial 是 JDK 1.3.1 之前新生代的唯一选择,虽然看起来这个垃圾收集器好像老而无用了。但是事实上它依然是 HotSpot 虚拟机运行在客户端模式下的默认新生代垃圾收集器,有着优于其他收集器的地方,那就是简单而高效(与其他收集器的单线程相比),对于内存资源受限的环境,它是所有收集器里额外内存消耗(Memory Footprint)最小的;
ParNew 收集器
ParNew 收集器实际上是 Serial 收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括 Serial 收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 收集器完全一一致。
ParNew 收集器除了支持多线程并行收集外,其他与 Serial 收集器相比并没有太多的创新之处,但它却是不少运行在服务端模式下的 HotSpot 虚拟机,尤其是 JDK 7 之前的遗留系统首选的新生代收集器,其中有一个与功能、性能无关但其实很重要的原因:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。
ParNew 收集器在单核心处理器的环境中绝对不会有比 Serial 收集器更好的效果。它默认开启的收集线程与处理核心数量相同,在处理器核心非常多的环境下,可以使用 -XX:ParallelGCThreads
来限制垃圾回收器的线程数。
Parallel Scavenge 收集器
Parallel Scavenge 收集器也是一款新生代收集器,同样是基于标记-复制算法实现的收集器,其从表面上看与 ParNew 十分相似,但它的关注点与其他收集器不同,CMS 等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而 Parallel Scavenge
收集器的目标则是达到一个可控制的吞吐量(Throughput)。
所谓吞吐量就是处理器用于运行用户代码的时间与处理器总消耗的时间的比值,即:
Tenured generation
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本,同样也是一个单线程工作的收集器,使用标记整理算法。
这个收集器的主要意义也是提供客户端模式下的 HotSpot 虚拟机使用。如果在服务端模式下,它也可能有两种用途:一种是在 JDK 5 以前的版本中与 Parallel Scavenge 收集器搭配使用,另外一种就是做为 CMS 收集器发生失败时的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记整理算法。
其主要与 Parallel Scavenge 做搭配。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。从名字上就可以看出 CMS 收集器是基于标记-清除算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为四个步骤,包括:
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发标记(CMS concurrent sweep)
其中并发标记以及重新标记这两个步骤仍然需要“Stop The World”。
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;
并发标记就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时很长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍微长一些,但也远比并发标记阶段的时间短;
最后是并发清除阶段,清除删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程并发进行的。
CMS 的优点很明显:并发收集、低停顿。但它也有很明显的缺点:
-
CMS收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程(或者说处理器的计算能力)而导致应用线程变慢,降低总吞吐量。
-
由于CMS收集器无法处理“浮动垃圾”(Floating Garbage),有可能出现”Concurrent Mode Failure“失败进而导致另一次完全”Stop The World“的 Full GC 的产生。
浮动垃圾:并发清理阶段用户线程还在运行,这段时间内就可能产生新的垃圾,新的垃圾在此次 GC 无法清除,只能等到下次清理。这些垃圾有个专业的名词:浮动垃圾;
-
CMS 是一款基于”标记-清除“的算法实现的垃圾收集器,这意味着收集结束时会有大量的空间碎片产生。空间碎片过多时,将会给大对象的分配带来很大的麻烦,往往会出现老年代还有很多剩余空间,但就是无法找到足够大的连续空间来分配当前对象。
Garbage First
为解决CMS算法产生空间碎片和其它一系列的问题缺陷,HotSpot提供了另外一种垃圾回收策略,G1(Garbage First)算法,通过参数-XX:+UseG1GC
来启用,该算法在JDK 7u4版本被正式推出,官网对此描述如下:
The Garbage-First (G1) collector is a server-style garbage collector, targeted for multi-processor machines with large memories. It meets garbage collection (GC) pause time goals with a high probability, while achieving high throughput. The G1 garbage collector is fully supported in Oracle JDK 7 update 4 and later releases. The G1 collector is designed for applications that:
Can operate concurrently with applications threads like the CMS collector.
Compact free space without lengthy GC induced pause times.
Need more predictable GC pause durations.
Do not want to sacrifice a lot of throughput performance.
Do not require a much larger Java heap.
在 G1 算法中,采用了另外一种完全不同以往的组织堆内存,堆内存被划分为多个大小相等的内存块(Region),每个Region是逻辑连续的一段内存,结构如下:
每个Region被标记了E、S、O和H,说明每个Region在运行时都充当了一种角色,其中H是以往算法中没有的,它代表Humongous,这表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
G1 中提供了三种模式垃圾回收模式,young GC、mixed GC 和 full GC,在不同的条件下被触发。
Young GC
发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次 Young GC,这种触发机制和之前的 young GC 差不多,执行完一次 Young GC,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。
Mixed GC
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 mixed gc,该算法并不是一个 Old GC,除了回收整个 Young region,还会回收一部分的 Old Region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些 Old region 进行收集,从而可以对垃圾回收的耗时时间进行控制。
mixed GC 的执行过程有点类似 CMS,主要分为以下几个步骤:
- initial mark: 初始标记过程,整个过程 STW,标记了从GC Root可达的对象
- concurrent marking: 并发标记过程,整个过程 GC collector线程与应用线程可以并行执行,标记出GC Root可达对象衍生出去的存活对象,并收集各个Region的存活对象信息
- remark: 最终标记过程,整个过程 STW,标记出那些在并发标记过程中遗漏的,或者内部引用发生变化的对象
- clean up: 垃圾清除过程,如果发现一个Region中没有存活对象,则把该 Region 加入到空闲列表中
Full GC
如果对象内存分配速度过快,Mixed GC 来不及回收,导致老年代被填满,就会触发一次 Full GC,G1 的 Full GC 算法就是单线程执行的 serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免 Full GC.