vlambda博客
学习文章列表

垃圾回收(四)垃圾回收器2

七种经典垃圾回收器

串行回收器:Serial、Serial Old;
并行回收器:ParNew、Parallel Scavenge、Parallel Old;
并发回收器:CMS、G1;

七种垃圾回收器与垃圾分代之间的关系

新生代回收器:Serial、ParNew、Parallel Scavenge;

老年代回收器:Serial Old、Parallel Old、CMS;

整堆回收器:G1;


垃圾回收(四)垃圾回收器2

垃圾回收器的组合关系:两个收集器之间有连线,表明可以搭配使用;其中,Serial Old作为CMS出现“Concurrent Mode Failure”失败的后备预案;在JDK 8时将Serial+CMS、ParNew+Serial Old这两个组合声明为废弃,并在JDK 9 中完全取消了这些组合的支持,即:移除;Parallel Scavenge+Serial Old在JDK 14中弃用;JDK 14中,删除CMS垃圾回收器。

垃圾回收(四)垃圾回收器2

Serial回收器:新生代、复制算法、串行回收、STW机制

Serial Old回收器:老年代、标记-压缩算法、串行回收、STW机制

这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或者一条收集线程去完成垃圾收集工作,更重要的它在进行垃圾收集时,必须停止其他所有的工作线程,直到它收集结束。所以在垃圾收集过程中,没有线程交互的开销,可以获得较高的效率。

垃圾回收(四)垃圾回收器2

ParNew回收器:新生代、复制算法、并行回收、STW机制

ParNew收集器可以说是Serial收集器的多线程版本;对于新生代,回收次数频繁、使用并行方式高效;对于老年代,回收次数少,使用串行方式节省资源。在多CPU环境下,可以充分利用多CPU等硬件资源优势,使用ParNew收集器可以提升程序吞吐量;但是在单个CPU的环境下,如果使用ParNew收集器,系统会频繁的进行线程切换,产生额外开销,其效率不一定比Serial收集器高。

垃圾回收(四)垃圾回收器2

Parallel Scavenge(Parallel)回收器:新生代、复制算法、吞吐量优先、并行回收、STW机制

Parallel Old回收器:老年代、标记-压缩算法、并行回收、STW机制

与ParNew的重要区别:吞吐量优先、自适应调节策略;高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在 后台运算 而不需要太多交互的任务。

垃圾回收(四)垃圾回收器2

参数设置:
  • -XX:+UserParallelGC 手动设置年轻代使用Parallel并行收集器;

  • -XX:+UserParallelOldGC 手动设置老年代使用Parallel Old并行收集器,与Parallel互相激活;

  • -XX:ParallelGCThreads 设置年轻代并发收集器的线程数,一般与CPU数量相同。当CPU数量小于8个,ParallelGCThreads的值默认等于CPU的数量,否则,ParallelGCThreads的值默认等于3+[5*CPU_Count]/8;

  • -XX:UseAdaptiveSizePolicy 设置Parallel具有自适应调节策略。在这种模式下,会自动的设置年轻代的大小、Eden和Survivor的比例、晋升老年代的年龄,已达到在堆大小、吞吐量和停顿时间之间的平衡点。

CMS回收器(Concurrent-Mark-Sweep):低延迟(强交互),并发回收(垃圾回收线程与用户程序线程同时工作)、标记-清除算法、STW机制

尽可能的缩短垃圾回收时用户线程的停顿时间。

整个过程分为四个主要阶段:
  • 初始标记(Initial-Mark)阶段:在这个阶段,暂停所有工作进程,主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程,由于直接关联对象比较少,所以这里的速度非常快。

  • 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时长但是不需要停顿用户线程。

  • 重新标记(Remark)阶段:由于在并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那部分对象(标记阶段不确定是否为垃圾的对象)的标记记录,运行时间远比并发标记阶段的时间短。

  • 并发清除(Concurrent-Sweep)阶段:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是与用户线程并发。

另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,要确保用户线程有足够的内存可用。因此,CMS收集器不能等到老年代几乎被填满了再进行收集,而是当堆内存使用率达到某个阈值时,便开始进行回收。否则,会出现一次“Concurrent Mode Failure”失败,这时虚拟机会启动后备方案:使用Serial Old收集器进行老年代的垃圾收集。
CMS使用标记-清除算法,导致出现内存碎片,但是还是不能使用标记-压缩算法,因为在标记清除阶段,垃圾回收线程与用户线程并发执行,标记-压缩算法需要对内存空间进行规整,而用户线程正在运行,无法对内存进行移动。同时,标记-压缩算法还会增加延迟。
优点:并发收集;低延迟;
弊端:
  • 产生内存碎片,埋下一颗定时炸弹;

  • 对CPU资源非常敏感:虽然不会导致用户线程停顿,但会占用CPU资源,导致应用程序执行变慢,降低吞吐量;

  • 无法处理浮动垃圾:在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有及时回收;

参数设置:
  • -XX:UseConcMarkSweepGC 手动指定使用CMS,开启该参数后,会自动将-XX:UseParNewGC打开,即:ParNew(年轻代)+CMS(老年代)+Serial Old(替补);

  • -XX:CMSInitiatingOccupanyFraction 设置堆内存使用率的阈值,一旦达到阈值,便开始回收。jdk 5及以前默认为68%,jdk 6及以上默认为92%,如果内存增长缓慢,可以设置一个稍大的值,目的是为了有效的降低Full GC的次数;

  • -XX:UseCMSCompactAtFullCollection 用于指定在执行完Full GC后对内存进行压缩整理;

  • -XX:CMSFullGCsBeforeCompaction 设置在执行多少次Full GC后对内存空间进行压缩整理;

  • -XX:ParallelCMSThreads 设置CMS线程数量,默认启动的线程数为(ParallelGCThreads+3)/4,ParallelGCThreads是年轻代并行收集器的线程数;

JDK 9中CMS被标记为Deprecate;JDK 14中删除了CMS垃圾收集器,但如果使用了CMS的话,JVM并不会报错,而是用其他垃圾收集器代替(G1)。

以上总结

  • 如果你想要最小化的使用内存和并行开销,请选Serial GC;

  • 如果你想要最大化的应用程序吞吐量,请选Parallel GC;

  • 如果你想要最小化GC的中断或停顿时间,请选CMS GC;



补(个人见解):

Minor GC 是从年轻代空间(包括 Eden 和 Survivor 区域)回收内存;
Major GC 是清理老年代;
Full GC 是清理整个堆空间—包括年轻代和老年代;
但是许多 Major GC 是由 Minor GC 触发的,所以很多情况下将这两种 GC 分离是不太可能的,同时,Minor GC与Full GC也经常混用,所以,我认为最好避免以 Minor、Major、Full GC 这种方式来思考问题,而应该监控应用延迟或者吞吐量,然后将 GC 事件和结果联系起来。