因为新生代和老年代采用回收算法的不同,垃圾收集器相应地也分为新生代收集器和老年代收集器。其中新生代收集器主要有Serial收集器、ParNew收集器和Parallel Scavenge收集器。老年代收集器主要有Serial Old收集器、Parallel Old收集器和CMS收集器。当然还包括了一款全新的、新生代老年代通用的G1收集器。各款收集器的搭配使用如下图所示,其中有连线的代表收集器可以搭配使用,没有连线的收集器表示不能搭配使用。
上面有7中收集器,分为两块,上面为新生代收集器,下面是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。
1.新生代垃圾收集器
1.1Serial(串行GC)收集器
Serial收集器是一个新生代收集器,单线程执行,使用复制算法。在进行垃圾回收的时候仅使用单条线程并且在回收的过程中会挂起所有的用户线程(Stop The World)。Serial收集器是JVM client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。
特别注意,Stop-The-World会挂起应用线程,造成应用的停顿。
1.2ParNew收集器
ParNew收集器作用于新生代,是一个多线程收集器,基于复制算法实现。相对于Serial收集器而言,在垃圾回收的时候会同时使用多条线程进行回收,但是它跟Serial收集器一样,在回收过程中也是会挂起所有的用户线程,从而造成应用的停顿。
1.3Parallel Scavenge(并行回收GC)收集器
Parallel Scavenge收集器同样作用于新生代,并且也是采用多线程和复制算法来进行垃圾回收。Parallel Scavenge收集器关注的是吞吐量,即使得应用能够充分使用CPU。它与ParNew收集器一样,在回收过程会挂起所有的用户线程,造成应用停顿。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。
2.老年代垃圾收集器
2.1 Serial Old收集器
Serial Old收集器作用于老年代,采用单线程和标记-整理算法来实现垃圾回收。在回收垃圾的时候同样会挂起所有用户线程,造成应用的停顿。一般来说,老年代的容量都比新生代要大,所以当发生老年代的垃圾回收时,STW经历的时间会比新生代所用的时间长得多。该收集器是JVM client模式下默认的老年代收集器。
Serial Old收集器还有一个重要的用途是作为CMS收集器的后备方案,在并发收集发生Concurrent Mode Failure的时候使用,进行内存碎片的整理。
2.2 Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多线程和标记-整理算法来实现老年代的垃圾回收。这个收集器主要是为了配合Parallel Scavenge收集器的使用,即当新生代选择了Parallel Scavenge收集器的情况下,老年代可以选择Parallel Old收集器。
在JDK1.6以前并没有提供Parallel Scavenge收集器,所以在1.6版本以前,Parallel Scavenge收集器只能与Serial Old收集器搭配使用。
2.3CMS(并发GC)收集器
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要注意以下两个关键词:
- concurrent mode failure
- promotion failed
对于采用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收集器对CPU资源非常敏感。在并发阶段,虽然不会导致用户线程停顿,但是会占用CPU资源而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是:(CPU数量 3) / 4。
- 在默认设置下,CMS收集器在老年代使用了68%的空间时就会被激活,也可以通过参数-XX:CMSInitiatingOccupancyFraction的值来提供触发百分比,以降低内存回收次数提高性能。要是CMS运行期间预留的内存无法满足程序其他线程需要,就会出现“Concurrent Mode Failure”失败,这时候虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiatingOccupancyFraction设置的过高将会很容易导致“Concurrent Mode Failure”失败,性能反而降低。
- CMS是基于“标记-清除”算法实现的收集器,使用“标记-清除”算法收集后,会产生大量碎片。空间碎片太多时,将会给对象分配带来很多麻烦,比如说大对象,内存空间找不到连续的空间来分配不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:UseCMSCompactAtFullCollection开关参数,用于在Full GC之后增加一个碎片整理过程,还可通过-XX:CMSFullGCBeforeCompaction参数设置执行多少次不压缩的Full GC之后,跟着来一次碎片整理过程。
3.G1收集器
为解决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垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间,该设计主要针对如下应用场景:
- 垃圾收集线程和应用线程并发执行,和CMS一样
- 空闲内存压缩时避免冗长的暂停时间
- 应用需要更多可预测的GC暂停时间
- 不希望牺牲太多的吞吐性能
- 不需要很大的Java堆 (翻译的有点虚,多大才算大?)
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会被放入空闲列表中,等待下次被使用。
参数 | 含义 |
---|---|
-XX:MaxGCPauseMillis | 设置G1收集过程目标时间,默认值200ms |
-XX:G1NewSizePercent | 新生代最小值,默认值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默认值60% |
mixed gc
当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择这些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。所添加old region的确切数量由一系列参数控制。 mixed gc的执行过程有点类似cms,主要分为以下几个步骤:
- 初始标记阶段:在此阶段,G1 GC 对根进行标记。该阶段与常规的 (STW) 年轻代垃圾回收密切相关。
- 根区域扫描阶段:G1 GC 在初始标记的存活区扫描对老年代的引用,并标记被引用的对象。该阶段与应用程序(非 STW)同时运行,并且只有完成该阶段后,才能开始下一次 STW 年轻代垃圾回收。
- 并发标记阶段:G1 GC 在整个堆中查找可访问的(存活的)对象。该阶段与应用程序同时运行,可以被 STW 年轻代垃圾回收中断。
- 重新标记阶段:该阶段是 STW 回收,帮助完成标记周期。G1 GC 清空 SATB 缓冲区,跟踪未被访问的存活对象,并执行引用处理。
- 清理阶段:在这个最后阶段,G1 GC 执行统计和 RSet 净化的 STW 操作。在统计期间,G1 GC 会识别完全空闲的区域和可供进行混合垃圾回收的区域。清理阶段在将空白区域重置并返回到空闲列表时为部分并发。
full gc
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc,G1的full gc算法就是单线程执行的serial old gc,会导致异常长时间的暂停时间,需要进行不断的调优,尽可能的避免full gc.
4.GC日志
了解GC日志可以帮助我们更好地排查一些线上问题,如OOM、应用停顿时间过长等等。GC日志对我们进行JVM调优也是很有帮助的。采用不同的GC收集器所产生的GC日志的格式会稍微不同,但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性。
具有一定共性的的GC日志格式大致如下所示:
代码语言:javascript复制<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>]
- datestamp : 表示GC日志产生的时间点,如果指定的jvm参数是-XX: PrintGCTimeStamps,那么输出的是相对于虚拟机启动时间的时间戳,如果指定的是-XX: PrintGCDateStamps,那么输出的是具体的时间格式,可读性更高
- GC : 表示发生GC的类型,有GC(代表MinorGC)和FullGC两种情况
- collector : 表示GC收集器类型,取值可能是DefNew、ParNew、PSYoungGen、Tenured、ParOldGen、PSPermGen等等
- start occupancy1 : 表示发生回收之前占用的内存空间
- end occupancy1 : 表示发生回收以后还占用的内存空间
- total size1 : 该堆区域所拥有的总内存空间
- pause time1 : 发生垃圾收集的时间
- start occupancy2 : 表示回收前Java堆内存总占用空间
- end occupancy2 : 表示回收后Java堆内存还占用的总空间
- total size2 : 表示Java堆内存总空间
- pause time2 : 表示整个堆回收消耗时间
- -XX: PrintGCDetails : 表示输出GC的详细情况
- -XX: PrintGCDateStamps : 指定输出GC时的时间格式,比-XX: PrintGCTimeStamps可读性更高
- -Xloggc : 指定gc日志的存放位置