理解JVM(七):垃圾回收器

1,068 阅读6分钟

一些概念

并行(Parallel)

指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。

并发(Concurrent)

指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。

吞吐量

CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

HotSpot虚拟机的垃圾回收器

Serial

  • 最基本的单线程垃圾收集器。使用一个CPU或一条收集线程去执行垃圾收集工作。
  • 工作时会Stop The World,暂停所有用户线程,造成卡顿。适合运行在Client模式下的虚拟机。
  • 用作新生代收集器,复制算法。

ParNew

  • Serial收集器的多线程版本,和Serial的唯一区别就是使用了多条线程去垃圾收集。
  • 除了Serial,只有它可以和CMS搭配使用的收集器。
  • 用作新生代收集器,复制算法。

Parallel Scavenge

  • 用作新生代收集器,复制算法。
  • 关注高吞吐量,可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
  • Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

Serial Old

  • Serial收集器的老年代版本,单线程,标记-整理 算法。
  • 一般用于Client模式的虚拟机。
  • 当虚拟机是Server模式时,有2个用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用 ,另一种用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

Parallel Old

  • Parallel Scavenge收集器的老年代版本,使用多线程和 标记-整理 算法。在JDK 1.6中开始提供。
  • 在注重吞吐量的场合,配合Parallel Scavenge收集器使用。

CMS(Concurrent Mark Sweep)

  • 一种以获取最短回收停顿时间为目标的收集器。适合需要与用户交互的程序,良好的响应速度能提升用户体验。
  • 基于 标记—清除 算法。适合作为老年代收集器。
  • 收集过程分4步:
    • 初始标记(CMS initial mark):只是标记一下GC Roots能直接关联到的对象,速度很快,会Stop The World
    • 并发标记(CMS concurrent mark):进行GC Roots Tracing(可达性分析)的过程。
    • 重新标记(CMS remark):会Stop The World。为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般比初始标记阶段稍长些,但远比并发标记的时间短。
    • 并发清除(CMS concurrent sweep):回收内存。
  • 耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以时并发执行的。
  • 缺点:
    • 并发阶段,虽然不会导致用户线程暂停,但会占用一部分线程(CPU资源),导致应用变慢,吞吐量降低。默认启动收集线程数是(CPU数量+3)/4。即当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大。
    • 无法清除浮动垃圾。并发清除阶段,用户线程还在运行,还会产生新垃圾。这些垃圾不会在此次GC中被标记,只能等到下次GC被回收。
    • 标记-清除 算法会产生大量不连续内存,导致分配大对象时内存不够,提前触发Full GC。

G1

  • 在JDK1.7提供的先进垃圾收集器。
  • 既适合新生代,也适合老年代。
  • 空间整合:使用 标记-整理 算法,不产生碎片空间。
  • 整个Java堆被分为多个大小相同的的块(region)。新生代和老年代不再是物理隔离的,而是一部分region块组成的集合。
  • 默认把堆平均分成2048个region,最小1M,最大32M,必须是2的幂次方,可以通过-XX:G1HeapRegionSize参数指定。region分为4种:
    • E:eden区,新生代
    • S:survivor区,新生代
    • O:old区,老年代
    • H:humongous区,用来放大对象。当新建对象大小超过region大小一半时,直接在新的一个或多个连续region中分配,并标记为H
  • 可预测的停顿时间:估算每个region内的垃圾可回收的空间以及回收需要的时间(经验值),记录在一个优先列表中。收集时,优先回收价值最大的region,而不是在整个堆进行全区域回收。这样提高了回收效率,得名:Garbage-First。
  • G1中有2种GC:
    • young GC:新生代eden区没有足够可用空间时触发。存活的对象移到survivor区或晋升old区。
    • mixed GC:当old区对象很多时,老年代对象空间占堆总空间的比值达到阈值(-XX:InitiatingHeapOccupancyPercent默认45%)会触发,它除了回收年轻代,也回收 部分 老年代(回收价值高的部分region)。
  • mixed GC回收步骤:
    • 初始标记(Initial Marking):只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。这阶段需要停顿线程(STW),但耗时很短,共用YGC的停顿,所以一般伴随着YGC发生。
    • 并发标记(Concurrent Marking):进行可达性分析,找出存活对象,耗时长,但可与用户线程并发执行。
    • 最终标记(Final Marking):修正并发标记阶段用户线程运行导致的变动记录。会STW,但可以并行执行,时间不会很长。
    • 筛选回收(Live Data Counting and Evacuation):根据每个region的回收价值和回收成本排序,根据用户配置的GC停顿时间开始回收。
  • 当对象分配过快,mixed GC来不及回收,G1会退化,触发Full GC,它使用单线程的Serial收集器来回收,整个过程STW,要尽量避免这种情况。
  • 当内存很少的时候(存活对象占用大量空间),没有足够空间来复制对象,会导致回收失败。这时会保留被移动过的对象和没移动的对象,只调整引用。失败发生后,收集器认为存活对象被移动了,有足够空间让应用程序使用,于是用户线程继续工作,等待下一次触发GC。如果内存不够,就会触发Full GC。

参考G1的详细介绍