因为新生代和老年代采用回收算法的不同,垃圾收集器相应地也分为新生代收集器和老年代收集器。其中新生代收集器主要有Serial收集器、ParNew收集器和Parallel Scavenge收集器。老年代收集器主要有Serial Old收集器、Parallel Old收集器和CMS收集器。当然还包括了一款全新的、新生代老年代通用的G1收集器。各款收集器的搭配使用如下图所示,其中有连线的代表收集器可以搭配使用,没有连线的收集器表示不能搭配使用。
上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。在进行垃圾回收的时候仅使用单条线程并且在回收的过程中会挂起所有的用户线程(Stop The World)。Serial收集器是JVM client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
特别注意,Stop-The-World会挂起应用线程,造成应用的停顿。
ParNew收集器作用于新生代,是一个多线程收集器,基于复制算法实现。相对于Serial收集器而言,在垃圾回收的时候会同时使用多条线程进行回收,但是它跟Serial收集器一样,在回收过程中也是会挂起所有的用户线程,从而造成应用的停顿。
1.3Parallel Scavenge(并行回收GC)收集器
Parallel Scavenge收集器同样作用于新生代,并且也是采用多线程和复制算法来进行垃圾回收。Parallel Scavenge收集器关注的是吞吐量,即使得应用能够充分使用CPU。它与ParNew收集器一样,在回收过程会挂起所有的用户线程,造成应用停顿。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
Serial Old收集器作用于老年代,采用单线程和标记-整理算法来实现垃圾回收。在回收垃圾的时候同样会挂起所有用户线程,造成应用的停顿。一般来说,老年代的容量都比新生代要大,所以当发生老年代的垃圾回收时,STW经历的时间会比新生代所用的时间长得多。该收集器是JVM client模式下默认的老年代收集器。
Serial Old收集器还有一个重要的用途是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure的时候使用,进行内存碎片的整理。
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法来实现老年代的垃圾回收。这个收集器主要是为了配合Parallel Scavenge收集器的使用,即当新生代选择了Parallel Scavenge收集器的情况下,老年代可以选择Parallel Old收集器。
在JDK1.6以前并没有提供Parallel Scavenge收集器,所以在1.6版本以前,Parallel Scavenge收集器只能与Serial Old收集器搭配使用。
CMS(Concurrent Mark Sweep)收集器是一款真正实现了并发收集的老年代收集器。CMS收集器以获取最短回收停顿时间为目标,采用多线程并发以及标记-清除算法来实现垃圾回收。CMS只在初始化标记和重新标记阶段需要挂起用户线程,造成一定的应用停顿(STW),而其他阶段收集线程都可以与用户线程并发交替进行,不必挂起用户线程,所以并不会造成应用的停顿。CMS收集器可以最大程度地减少因垃圾回收而造成应用停顿的时间。
CMS垃圾收集分为以下几个阶段:
(1) 初始化标记 (inital mark)
这个阶段仅仅是标记了GC Roots能够直接关联到的对象,速度很快,所以基本上感受不到STW带来的停顿。
(2) 并发标记 (concurrent mark)
并发标记阶段完成的任务是从第一阶段收集到的对象引用开始,遍历所有其他的对象引用,并标记所有需要回收的对象。这个阶段,收集线程与用户线程并发交替执行,不必挂起用户线程,所以并不会造成应用停顿。
(3) 并发预清除 (concurrent-pre-clean)
并发预清除阶段是为了下一个阶段做准备,为的是尽量减少应用停顿的时间。
(4) 重新标记 (remark)
这个阶段将会修正并发标记期间因为用户程序继续运作而导致标记产生变动的那部分对象的标记记录(有可能对象重新被引用或者新对象可以被回收)。这个阶段的停顿时间比初始标记阶段要长一些,但是远比并发标记的时间短。
(5) 并发清除 (concurrent sweep)
这个阶段将真正执行垃圾回收,将那些不被使用的对象内存回收掉。
(6) 并发重置 (concurrent reset)
收集器做一些收尾的工作,以便下一次GC周期能有一个干净的状态。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以整体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
使用CMS要注意以下两个关键词:
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
promotion failed是在进行Minor GC时,新生代的survivor区放不下,对象只能放入老年代,而此时老年代也放不下造成的。
concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代(满足一定年龄的对象或者大对象),而此时老年代空间不足造成的。
通常我们把发生在新生代的垃圾回收称为Minor GC,而把发生在老年代的垃圾回收称为Major GC,而FullGC是指整个堆内存的垃圾回收,包括对新生代、老年代和持久代的回收。一般情况下应用程序发生Minor GC的次数要远远大于Major GC和Full GC的次数。
在讲解GC的时候会涉及到并行和并发两个概念。在这里,并行指的是多个GC收集线程之间并行进行垃圾回收。而并发指的是多个GC收集线程与所有的用户线程能够交替执行。
CMS收集器的优点:并发收集、低停顿,但是CMS还远远达不到完美,主要有三个显著缺点:
为解决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:
翻译如下:
G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:
G1中提供了三种模式垃圾回收模式,young gc、mixed gc 和 full gc,在不同的条件下被触发。
发生在年轻代的GC算法,一般对象(除了巨型对象)都是在eden region中分配内存,当所有eden region被耗尽无法申请内存时,就会触发一次young gc,这种触发机制和之前的young gc差不多,执行完一次young gc,活跃对象会被拷贝到survivor region或者晋升到old region中,空闲的region会被放入空闲列表中,等待下次被使用。
参数 | 含义 |
---|---|
-XX:MaxGCPauseMillis | 设置G1收集过程目标时间,默认值200ms |
-XX:G1NewSizePercent | 新生代最小值,默认值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默认值60% |
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择这些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。所添加old region的确切数量由一系列参数控制。 mixed gc的执行过程有点类似cms,主要分为以下几个步骤:
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
了解GC日志可以帮助我们更好地排查一些线上问题,如OOM、应用停顿时间过长等等。GC日志对我们进行JVM调优也是很有帮助的。采用不同的GC收集器所产生的GC日志的格式会稍微不同,但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性。
具有一定共性的的GC日志格式大致如下所示:
<datestamp>:[GC[<collector>:<start occupancy1>-><end occupancy1>(total size1),<pause time1> secs]<start occupancy2>-><end occupancy2>(total size2),<pause time2> secs] [Times:<user time> <system time>, <real time>]