JVM垃圾收集器介绍

曾经看到过一句话:如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。目前商业化虚拟机中常用的垃圾收集器有8种:新生代:Serial、ParNew、Parallel Scavenge,老年代:CMS、Serial Old、Parallel Old,整堆:G1、ZGC。

image-20200506000424241

图中连线的两个收集器是可以搭配使用,其所处区域表示收集器的作用域。

1. 垃圾收集器运行方式

垃圾收集器在运行方式上又细分为串行、并行、并发三种。

  • 串行收集器:GC线程和其他用户线程是串行的,也就是在进行垃圾回收的时候,其他的线程需要排队等待,直到收集器的线程完成工作,这类收集器停顿时间长、吞吐量低,适用于单C机器使用,使用JVM参数-XX:+UseSerialGC开启,
  • 并行收集器是采用多线程串行的方式进行垃圾回收,适用于多C机器,在多个线程执行垃圾检测回收时,可能会因为线程之间竞争CPU资源而发生长时间的停顿,体验极差,不推荐使用,可以使用JVM参数-XX:+UseParallelGC-XX:+UseParallelOldGC开启,并且可以使用-XX:ParallelGCThreads=<thread_nums>指定GC的线程数量,一般不要高于CPU数量,否则就容易gg;是jdk1.8中默认使用的垃圾收集器;
  • 并发收集器是目前使用较多的一类收集器,与并行收集器不同的是它采用多个线程并行执行去进行垃圾检测和回收,停顿时间短,吞吐量大,可以使用JVM参数-XX:+UseConcMarkSweepGC开启,可以搭配-XX:+UseParNewGC一同使用;

2. 吞吐量

说到垃圾收集就不得不提吞吐量这个概念,吞吐量是指用户代码运行时间与虚拟机总运行时间的比值:吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),比如虚拟机总共运行了100分钟,期间进行了一次垃圾收集,耗时0.01分钟,那么吞吐量就是99.99%。

吞吐量越高则表示GC总耗时越少,服务性能则越高。

3. GC类型

  1. Minor GC:也叫作Young GC,只作用于新生代

    我们知道新生代分为一个Eden区和两个Survivor区,常规对象被创建之后都会有限分配到Eden区,但是Eden区的空间大小是有限的,当Eden可用空间不足时,就会触发Minor GC,Minor GC之后在Eden区仍存活的对象会被移动到Survivor区。

    在发生Minor GC时,Eden区和Survivor的from区存活的对象全部被复制到当前Survivor的to区,然后将Survivor的from和to区指针互换。

    • 在发生Minor GC的时候,若from区内对象年龄达到了晋升老年代的条件-XX:MaxTurningThreshold,则将这部分对象转移到老年代;

    • 若在Minor GC后,Eden区和Survivor的from区存活对象的总空间大于Survivor的to区的大小,则优先将对象复制到to区,待to区存满之后,将剩余的对象转移到老年代,这叫过早提升。

  2. Old GC:只有CMS的concurrent collection这个模式,只作用于老年代

    CMS是一款基于并发、使用标记清楚算法的垃圾收集算法, 只针对老年代进行垃圾回收,

  3. Mixed GC:混合GC模式,同时收集新生代和老年代,目前只有G1是这种模式

  4. Full GC:收集整个堆,包括新生代、老年代和永久代(Metaspace)

4. 垃圾收集器介绍

新生代收集器
  • Serial收集器

    Serial收集器是初代收集器,历史最为悠久。它是一个采用了复制算法的单线程收集器,非常适用于单个CPU的环境,它会导致STW(Stop The World),在进行垃圾收集时必须暂停其他所有的工作线程,直至垃圾收集结束为止。收集工作由虚拟机在后台自动发起和完成,用户不可见。由于每次垃圾回收时都要暂停,若垃圾对象过多,那么进程暂停时间可能过长,这对很多应用来说是很不能接受的。

    Serial收集器的运行过程(Serial + Serial Old):

    image-20200505034217123

    Serial可以与老年代的CMS和Serial Old收集器配合工作,可以使用JVM参数-XX:+UseSerialGC开启。

    🌰:java -jar -XX:+UseSerialGC -XX:+UseConcMarkSweepGC xxx.jar

  • ParNew收集器

    ParNew是Serial的多线程版本,拥有Serial的所有功能(控制参数、收集算法、STW、对象分配规则、回收策略等),可以与老年代的CMS和Serial Old收集器配合工作。

    在单CPU环境中,ParNew不会比Serial更高效;在多CPU环境下,随着CPU的数量增加,GC线程数增加,STW的时间会大大缩小。

    ParNew默认开启的GC线程数与CPU的数量相同,可以使用JVM参数-XX:+UseParNewGC开启,并可以使用JVM参数-XX:ParallelGCThreads指定GC线程数

    ParNew收集器的运行过程(ParNew + Serial Old):

    image-20200505034308215

    🌰:java -jar -XX:+UseParNewGC -XX:ParallelGCThreads=2 -XX:+UseConcMarkSweepGC xxx.jar

  • Parallel Scavenge收集器

    Parallel Scavenge收集器是一款并行的多线程新生代收集器,也是使用的复制算法,强关注吞吐量, 高吞吐量可以更高效的利用CPU,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。吞吐量=运行用户代码时间/(运行用户代码时间+GC时间)。

    Parallel Scavenge是jdk1.8的默认新生代垃圾收集器,只能与老年代的Serial Old和Parallel Old两款收集器配合工作,不可与CMS搭配使用。

    收集器通过JVM参数-XX:MaxGCPauseMillis-XX:GCTimeRatio来控制最大GC停顿时间及吞吐量大小,另外还可以使用JVM参数-XX:+UseAdaptiveSizePolicy打开GC自适应调节策略。

    • -XX:MaxGCPauseMillis:参数值必须大于0,单位毫秒,但是这个值只能提醒收集器尽可能的保证在这个毫秒内完成GC,并不能一定保证每次GC都在这个时间内,系统会动态调小新生代内存,并缩小GC间隔,以降低吞吐量来保证STW的时间。为什么说是降低吞吐量,因为GC频率高了,GC总时间就长了,所以吞吐量就低了。
    • -XX:GCTimeRatio:参数值范围是0~100的整数,表示GC总时间占虚拟机运行时间的比率,比如将值设置为49,那么允许的最大GC时间占总时间的2%(1/(1+49))。
    • -XX:+UseAdaptiveSizePolicy:GC自适应调节策略,当打开这个参数之后,就不需要指定新生代的大小(-Xmn)、Eden与Survivor的比例(-XX:SurvivorRatio)、晋升了老年代对象年龄(-XX:PretenureSizeThreshold)等参数,虚拟机会根据当前虚拟机的运行情况收集性能监控信息,动态调整这些参数寻找最优的停顿时间和吞吐量。
小结

从上文中我们发现新生代的三个GC收集器都是使用的复制算法,这样可以提高回收效率,并且这也是以新生代的区域划分为基础的,我们知道新生代分为1个Eden区和2个Survivor区,每次Minor GC之后都会将Eden区存活的对象复制到Survivor区,然后将Survivor的from区存活的对象复制到to区,然后清空Eden和Survivor的from区,将Survivor的to区和from区切换身份,所以使用复制算法简单高效。

老年代收集器
  • CMS收集器

    CMS(Concurrent Mark Sweep)收集器是一款以获取最短回收停顿时间为目标的收集器,注重虚拟机的响应速度,收集器基于“标记-清除”算法,特别容易产生内存碎片。

    CMS收集器工作流程大概分为以下4个步骤:

    1. 初始化标记(CMS initial mark):会导致STW,仅仅只标记一下存活对象(从GC Roots能直接关联到的对象)
    2. 并发标记(CMS Concurrent mark):不会STW,进行GC Roots Tracing的过程,耗时最长
    3. 重新标记(CMS remark):会导致STW,再次标记主要是修正并发标记期间因为用户线程运行而导致标记的对象发生的变动,这一阶段的停顿时间也较长,但是比并发标记短很多
    4. 并发清除(CMS Concurrent Sweep)

    image-20200505040204157

    • 优点
      1. 并发收集
      2. 低停顿
    • 缺点
      1. 对CPU资源敏感
      2. 无法处理浮动垃圾
      3. 会产生内存碎片
  • Serial Old收集器

    Serial Old是Serial收集器的老年代收集器,它是一个单线程收集器,使用“标记-整理”算法,起初此款收集器主要用于client模式下的虚拟机,在server模式下,可以与新生代的Parallel Scavenge收集器搭配使用,并且也可以作为CMS收集器的备用方案,在发生Concurrent Mode Failure时自动切换到Serial Old收集器。

    工作流程与Serial相同,Serial+Serial Old:

    image-20200505034217123

  • Parallel Old收集器

    Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在jdk1.6才开始出现,在此之前如果新生代使用Parallel Scavenge收集器的话,老年代就只能选择Serial Old这一个收集器,而在Parallel Old出现之后,在注重吞吐量以及CPU资源敏感的场景下,都可以优先考虑Parallel Scavenge+Parallel Old

整堆收集器

说到整堆收集器就只有说说G1收集器了,G1收集器具有并行与并发、分代收集、空间整合和可预测的停顿等特点。

  • 收集器既并行又并发,能够充分利用多CPU,缩短STW的停顿时间
  • 因为G1能够管理整个堆,而不需要和其他的收集器搭配使用,虽然依然采用了分代模式,但它把堆分成了大小相等的若干个独立区域,相邻区域很可能是一个是新生代,一个是老年代。
  • 整个过程中不会产生内存碎片,整体使用的“标记-整理”算法,局部使用的是复制算法,这两种算法都不会产生内存碎片,适合长时间运行。
  • 低停顿的同时实现高吞吐量;G1除了追求低停顿处,还能建立可预测的停顿时间模型;可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒

5. 减少GC次数的手段

  1. 对象不使用时将其置为null
  2. 尽量不用System.gc()
  3. 尽量少用静态变量
  4. 使用StringBuffer代替String拼装字符串
  5. 分散对象创建和删除的时间
  6. 尽量少勇finallize方法
  7. 使用基本类型替代基本类型封装类
  8. 加大堆内存
  9. 慎用软引用、弱引用和虚引用