JVM垃圾收集器

708 阅读9分钟

前言

  • 前面讲了垃圾收集的机制(算法),本篇来讲讲垃圾收集算法是如何在垃圾收集器中实现的。

image.png

  • 垃圾收集器有好多种,目前为止没有一种垃圾收集器是完美的(如果有的话,就不需要这么多种了)。一般是根据实际业务场景来选择合适的垃圾收集器。

Serial收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

  • Serial收集器是单线程收集器。只会用一条垃圾收集线程工作,更重要的是它工作时,必须暂停其他所有工作线程(Stop The World)。新生代使用标记复制算法,老年代使用标记整理算法 image.png

Parallerl Scavenge(-XX:+UseParallerlGC -XX:+UseParallerlOldGC)

  • Parallerl可以看成是Serial的多线程版本。jdk1.8默认的垃圾收集器。默认收集线程数和cpu核数相同,可以用(-XX:ParallerlGCThreads)指定线程数。新生代使用标记复制算法,老年代使用标记整理算法 image.png
  • Parallerl的特点是它和其他收集器的关注点不同。CMS等收集器主要关注用户线程的停顿时间(用户体验),Parallerl关注达到一个可控制的吞吐量。
吞吐量=运行用户代码时间运行用户代码时间+运行垃圾收集时间吞吐量=\dfrac{运行用户代码时间}{运行用户代码时间 + 运行垃圾收集时间}

ParNew(-XX:+UseParNewGC)

  • 和Parallerl类似,区别是ParNew可以和CMS配合使用(ParNew在新生代使用,CMS在老年代)新生代使用标记复制算法,老年代使用标记整理算法

CMS(-XX:+UseConcMarkSweepGC)

  • CMS是以获取最短回收停顿时间为目标的收集器。第一次在HotSpot上真正实现了垃圾收集线程与用户线程(基本上)同时工作。标记清除算法
  • 整个工作分为四步
  1. 初始标记:暂停所有其他线程(STW),记录gc roots直接引用对象,速度很快。
  2. 并发标记:从gc roots直接引用对象开始遍历整个对象图。耗时较长但不需要停顿用户线程,同时由于用户程序还在进行,可能会导致已经标记过的对象状态发生变化。
  3. 重新标记:修正并发标记期间用户程序导致的那一部分标记状态变化的对象。这部分停顿比初始标记时间长,远远比并发标记时间短。主要用到三色标记中的增量更新算法来重新标记。
  4. 并发清理:开启用户线程,同时对未标记的对象进行清理。这个阶段,如果有新的对象产生,会被标记为黑色,不做任何处理。
  5. 并发重置:重置本次GC过程中的标记数据。 image.png
  • 可以看到,CMS特点很明显:并发收集、低停顿。缺点也很明显:
  1. 对cpu资源敏感,会和服务抢资源。
  2. 无法处理浮动垃圾(在并发标记和并发清理阶段产生的垃圾,只能等到下一次gc时处理)。
  3. 由于使用标记清理算法,会产生大量碎片。通过-XX:UseCMSCompactAtFullCollection可以在收集完后再做整理
  4. 执行过程中的不确定因素,如果这次垃圾收集还没处理完成,再次执行了垃圾收集,特别是并发标记和并发清理阶段,一边清理,系统一边运行,还没清理完就再次触发full gc,即“concurrent mode failure”,此时会进入STW,用Serial old垃圾收集器回收。
  • CMS其实没有把总的STW时间缩短,而是把大部分的行为都很用户并行,这样用户基本上感知不到停顿,这在大内存的时候尤为明显。小内存(1~3G)时,用Parallerl就行。
  • CMS核心参数:
  1. -XX:+UseConcMarkSweepGC:启动CMS
  2. -XX:ConcGCThreads:并发GC线程数
  3. -XX:+UseCMSCompactAtFullCollection:full gc后整理
  4. -XX:CMSFullGCsBeforeCompaction:多少次full gc之后整理一次,默认0,代表每次full gc都整理
  5. -XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发full gc,默认92,百分比。
  6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction),如果不写这个参数,JVM在第一次使用设定值后,后续则自动调整。
  7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引用,降低CMS在标记阶段时的开销,一般CMS耗时80%都在标记阶段。
  8. -XX:+CMSParallelInitialMarkEnabled:初始标记时多线程执行,缩短STW
  9. -XX:+CMSParallelRemarkEnabled:在重新标记阶段多线程执行,缩短STW

G1(-XX:+UseG1GC)

  • G1收集器主要针对多颗处理器及大容量的机器
  • G1把堆划为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region的大小等于堆大小/2048。也可以用参数-XX:G1HeapRegionSize指定Region大小。
  • G1中,新生代和老年代的概念依旧存在,但不是连续的空间,而是Region的集合。默认新生代占堆内存比重是5%。可以通过-XX:G1NewPercent来控制初始占比。在运行过程中,系统会给新生代增加Region,最多不超过60%。可以使用-XX:G1MaxNewPercent来调整。新生代中Eden和Survivor比例也是8:1:1。
  • 一个Region开始是新生代,进行垃圾回收后,可能会变为老年代。
  • G1转门为大对象设置了Humongous区域。判断大对象的规则是对象的大小超过了Region的50%。如果一个对象过大,会通过跨多个Humongous Region来存放。Humongous在full gc时,也会被回收。 image.png
  • G1的工作步骤如下:
  1. 初始标记(同CMS):暂停所有其他线程(STW),记录gc roots直接引用对象,速度很快。
  2. 并发标记(同CMS):从gc roots直接引用对象开始遍历整个对象图。耗时较长但不需要停顿用户线程,同时由于用户程序还在进行,可能会导致已经标记过的对象状态发生变化。
  3. 最终标记(同CMS):修正并发标记期间用户程序导致的那一部分标记状态变化的对象。这部分停顿比初始标记时间长,远远比并发标记时间短。主要用到三色标记中的增量更新算法来重新标记。
  4. 筛选回收:首先对各Region的回收价值和成本进行排序。根据用户希望的停顿时间(-XX:MaxGCPauseMillis设置)来制定回收计划。比如现在有1000个Region满了,回收需要500ms,但是用户期望是100ms,那么本次只收回300个Region。
  • 新生代和老年代的回收算法都是标记复制算法,将一个Region的数据复制到另一个Region中去,和CMS会产生内存碎片不同的是,G1的Region设定,几乎不会出现内存碎片。 image.png
  • 另外,G1在后台维护了一个优先列表,根据每次回收时允许的时间,优先选择回收价值大的Region。所以叫做G1(Garbage-First)。比如某个Region花100ms回收20m,另一个Region50ms能回收40m,G1会优先回收后者。
  • G1特点:
  1. 并行并发:充分利用cpu,使用多个cpu来缩短STW时间。
  2. 分代收集:保留了分代概念
  3. 空间整合:G1从整体上来看是标记整体,局部上看是复制算法。
  4. 可预测的停顿:通过-XX:MaxGCPauseMillis设置来指定停顿时间。当然不是随便设置期望值,也不是越短越好,如果每次收集的时间很短,无法清理出足够的空间,那么垃圾会逐渐堆积,最终引发full gc反而降低性能。通常设置为一百至三百毫秒为佳。

G1的垃圾收集分类

  • YoungGC

当eden区放满之后,并不会马上YoungGC,G1会计算回收时间,如果远小于预期时间,那么会新增Region继续给新对象。当下次接近预期时间时,才会触发YoungGC。

  • MixedGC

不是Full GC。老年代堆占用率达到参数(-XX:InitiatingHeapOccupancyPercent)时触发。回收所有YoungGC和部分Old以及大对象区。正常情况G1垃圾收集是先做MixedGC。主要使用复制算法。把各Region中存活的对象拷贝到别的Region中去,拷贝过程中如果发现没有足够的Region承载就会触发Full GC。

  • Full GC

停止系统程序。然后采用单线程进行标记、清理、整理。非常耗时。

G1收集参数

  • -XX:+UseG1GC:使用G1收集器
  • -XX:ParallelGCThreads:指定GC工作的线程数量
  • -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,必须是2的N次幂),默认划分为2048个分区。
  • -XX:MaxGCPauseMillis:目标暂停时间(200ms)
  • -XX:G1NewSizePercent:新生代内存初始化空间(默认堆的5%,配置时用整数,默认就是百分比)
  • -XX:G1MaxNewSizePercent:新生代最大内存空间
  • -XX:TargetSurvivorRatio:Survivor区填充容量(默认50%),Survivor中年龄1+年龄2+年龄n的多个年龄对象总和超过50%,就会把年龄n(含)以上的对象放入老年代
  • -XX:MaxTenuringThreshold:最大年龄阈值(默认15)
  • -XX:InitiatingHeapOccupancyPercent:老年代堆占用率达到参数时触发(默认45%),则执行新生代和老年代的混合收集MixedGC。
  • -XX:G1MixedGCLiveThresholdPercent:默认85%。region中存活的对象低于这个值时才会回收该region。
  • -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再回收,这样可以不至于让系统单次回收时间过长。
  • -XX:G1HeapWastePercent:默认5%。gc过程中空出来的region是否充足阈值,在MixedGC时,会有region空出来,如果空出来的region达到了堆内存的5%,此时会立即停止回收。

G1优化

核心在于调节-XX:MaxGCPauseMillis参数。设置太大会导致清理垃圾时垃圾太多,Survivor放不下,直接进入老年代,频繁触发MixedGC。

G1适合的场景

  1. 50%以上的堆被存活对象占用
  2. 对象分配和晋升的速度变化非常大
  3. 垃圾回收时间特别长,超过1秒
  4. 8G以上堆内存
  5. 停顿时间500ms内
  • 4G以下用parallel,4-8G可以用ParNew+CMS,8G以上用G1,几百G以上用ZGC

G1用SATB和CMS用增量更新的区别

SATB相对增量更新效率更高,因为不需要在重新标记阶段再度深度扫描被删除的对象。而增量更新会对GC Roots做深度扫描,G1很多对象都位于不同的region,所以G1做深度扫描代价会比CMS高。