jvm

JVM 系列 垃圾收集器

开启JVM探索新篇章

Posted by lichao modified on November 7, 2019

垃圾收集器的不断更新,只为消除或减少工作线程因内存回收而导致的停顿。

垃圾收集器

原理

GC调优目标

大多数情况下对 Java 程序进行 GC 调优, 主要关注两个目标:响应速度、吞吐量

  • 响应速度(Responsiveness) 响应速度指程序或系统对一个请求的响应有多迅速。比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应

  • 吞吐量(Throughput) 吞吐量关注在一个特定时间段内应用系统的最大工作量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求

GC 调优中,GC 导致的应用暂停时间影响系统响应速度,GC 处理线程的 CPU 使用率影响系统吞吐量

GC事件分类

根据垃圾收集回收的区域不同,垃圾收集主要通常分为 Young GC、Old GC、Full GC、Mixed GC:

  1. Young GC

新生代内存的垃圾收集事件称为Young GC(又称Minor GC),当JVM无法为新对象分配在新生代内存空间时总会触发 Young GC,比如 Eden 区占满时。新对象分配频率越高, Young GC 的频率就越高

Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代GC的造成的停顿,几乎可以忽略不计

  1. Old GC 、Full GC、Mixed GC

Old GC,只清理老年代空间的GC事件,只有CMS的并发收集是这个模式

Full GC,清理整个堆的GC事件,包括新生代、老年代、元空间等

Mixed GC,清理整个新生代以及部分老年代的GC,只有G1有这个模式

Java 提供的自动内存管理,可以归结为解决了对象的内存分配和回收的问题,前面已经介绍了内存回收,下面介绍几条最普遍的内存分配策略

内存分配策略

Java 提供的自动内存管理,可以归结为解决了对象的内存分配和回收的问题,前面已经介绍了内存回收,下面介绍几条最普遍的内存分配策略

  • 对象优先在 Eden 区分配 大多数情况下,对象在先新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Young GC

  • 大对象直接进入老年代 JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,代表不管多大都是先在Eden中分配内存),大于参数设置的阈值值的对象直接在老年代分配,这样可以避免对象在Eden及两个Survivor直接发生大内存复制

  • 长期存活的对象将进入老年代 对象每经历一次垃圾回收,且没被回收掉,它的年龄就增加1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象,将晋升到老年代中

  • 空间分配担保 当进行Young GC之前,JVM需要预估:老年代是否能够容纳Young GC后新生代晋升到老年代的存活对象,以确定是否需要提前触发GC回收老年代空间,基于空间分配担保策略来计算:

分配担保策略

continueSize:老年代最大可用连续空间

存储概览

Young GC 之后如果成功(Young GC后晋升对象能放入老年代),则代表担保成功,不用再进行 Full GC,提高性能;如果失败,则会出现 “promotion failed” 错误,代表担保失败,需要进行 Full GC

动态年龄判定 新生代对象的年龄可能没达到阈值(MaxTenuringThreshold参数指定)就晋升老年代,如果Young GC之后,新生代存活对象达到相同年龄所有对象大小的总和大于任一Survivor空间(S0 或 S1总空间)的一半,此时S0或者S1区即将容纳不了存活的新生代对象,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄

另外,如果 Young GC 后 S0 或 S1 区不足以容纳:未达到晋升老年代条件的新生代存活对象,会导致这些存活对象直接进入老年代,需要尽量避免

Serial

关键词:新生代,client模式,单线程

serial垃圾收集器 Serial 收集器是 Java 最基本、历史最悠久的收集器,曾经是新生代垃圾收集唯一的选择。特点是单线程回收、垃圾回收的时候Stop the World,即暂停所有工作线程,并采用复制算法进行垃圾回收,是Java虚拟机Client模式下默认的新生代收集器,能配合CMS收集器,在单个CPU的环境中没有线程交互的开销,可以获得最高的单线程收集效率。

ParNew

关键词:新生代,server模式

ParNew垃圾收集器 ParNew 收集器 是 Serial 收集器的多线程版本,除了使用了多线程之外,其他的行为(收集算法、stop the world、对象分配规则、回收策略等)同 Serial 收集器一样。 是许多运行在 Server 模式下的 JVM 中首选的新生代收集器,其中一个很重还要的原因就是除了 Serial 之外,也是新生代唯二能与CMS收集器配合工作的收集器。

Parallel Scavenge

ParallelScavenge垃圾收集器

Parallel Scavenge 也是新生代的收集器,使用标记复制算法且并行多线程收集。设计目标是达到一个可控的的吞吐量,适合后台计算型不需要太多交互的任务,与CMS收集器不兼容。

Serial Old(MSC)

serial垃圾收集器 Serial Old 是 Serial 收集器的老年代版本,单线程收集,使用标记整理算法,主要是Client模式下虚拟机的使用。在Server模式下,一是在JDK1.5之前与Parallel Scavenge收集器配合使用,而是作为CMS收集并发失败后的备胎收集器。

Parallel Old

ParallelScavenge垃圾收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,支持多线程并发收集,基于标记整理算法实现。配合Parallel Scavenge收集器,达到吞吐量优先的目的。

CMS

关键词:老年代,server 模式

cms垃圾收集器

基于 标记-清除 算法 CMS(Concurrent Mark Sweep 并发标记收集器) 收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验好。基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分 4 步:

  1. 初始标记:标记 GC Roots 能直接关联到的对象,速度快,但是需要“Stop The World”。JDK7 之前单线程,JDK8 之后并行,可以通过参数CMSParallelInitialMarkEnabled 调整。
  2. 并发标记:可以和用户线程并发执行,遍历整个 GC roots 关联的对象图,耗时较高。
  3. 重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”
  4. 并发清除:与用户线程并发,不需要移动对象 由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS 收集器的内存回收过程和用户线程是并发执行的。

新生代的规模一般都比老年代要小许多,新生代的回收也比老年代要频繁很多。收集器中的新生代与老年代之间的对象引用是使用 Remembered Set 来避免全堆扫描的

缺点

  1. CMS 收集器对 CPU 资源非常敏感:CMS 默认启动的收集线程数不少于 25% 的 CPU 资源,并且随着 CPU 数量的增加而下降
  2. CMS 收集器无法处理浮动垃圾。在垃圾回收时,需要预留有足够的内存空间给用户线程使用。在 JDK 1.5 的默认配置下,CMS 收集器在老年代使用了 68% 的空间后就被激活。在 JDK 1.6 中,CMS 收集器的启动阈值已经提升到了 92%。
  3. CMS 是一款基于“标记-清除”算法实现的收集器,会有大量空间碎片产生。

空间碎片过多时,将会给大对象分配带来很大麻烦。CMS会择机进行内存碎片的合并整理

G1

关键词:整个堆,服务端,标记-整理算法 及 标记-复制算法,化整为零的思想

G1(Garbage-First)是JDK7-u4才正式推出商用的收集器。G1是面向服务端应用的垃圾收集器。它的使命是未来可以替换掉CMS收集器。

G1 将整个 Java堆 划分为多个大小相等的独立区域 region,虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分region(不需要连续)的集合。

G1的内存布局如上,G1不再坚持固定大小以及固定数量的分代区域划分。而是把连续的java堆划分为大小相等的独立的区域(Region),每个Region根据需要扮演Eden,Survivor,Old空间,Region中有用于专门存储大对象的空间称为Humongous区域,用于存储超过一定大小的对象。G1的运作流程如下:

  1. 初始标记,基本与CMS类似,标记GC roots关联到的对象,并且修改TAMS(Top at Mark Start)指针),即从region中划分出一块区域用于并发标记时候分配新的对象,这块区域的对象在此次标记过程中默认不回收,此过程STW,耗时较短。
  2. 并发标记,基本与CMS类似,但是G1采用的是SATB来处理引用变动的对象。此阶段与用户线程并发,不STW。
  3. 最终标记,STW,耗时很短很短,扫描SATB中的对象。
  4. 筛选回收,此过程筛选回收价值高的Region,根据设置的最大的停顿参数-XX:MaxGCPauseMillis,决定回收那些区域。这也是G1名称的由来,回收价值高的区域。最后将区域中存活的对象拷贝到空Region中,清除掉旧的Region。由于涉及到移动对象,此步骤STW,并行移动对象。 存在的问题:
    1. 跨区域对象引用的问题,我们知道可以使用rember set,即记忆集。由于region众多,此记忆集至少耗费10%到20%的堆容量来维持收集器的工作。
    2. 并发标记阶段如何解决并发问题,G1采用的是STAB加TAMS(Top at Mark Start)的方式保证对象分配和回收的安全。如果内存分配速度过快,导致TAMS的区域无足够空间,会导致Full GC并产生一个长时间的STW。
    3. 怎么建立的可靠停顿模型?通过XX:MaxGCPauseMillis参数吗?感觉并不那么容易…

Shenandoah垃圾收集器

Shenandoah收集器比较特殊,与上述介绍的收集器和ZCG收集器都不一样,不是由Oracle公司所开发的,而是由RedHat公司独立开发,并在2014年贡献给了OpenJDK目前在HotSpot虚拟机和“官方”JDK中并不支持此收集器,只在开源的OpenJDK才会包含此收集器。 Shenandoah收集器基本源于 G1,与G1有相同的内存布局,存储大对象的Humongous区域,也是优先回收高价值的Region,它的设计目标是把任意大小的堆的垃圾回收的停顿时间限制在10毫秒以内。那么它与G1不同的地方在哪里呢

  1. 筛选回收阶段,G1是多收集线程并行回收,Shenandoah则与用户线程并发执行。
  2. G1存在分代,而Shenandoah默认不分代。
  3. Shenandoah抛弃了G1中的记忆集,转而使用连接矩阵(Connection Matrix)的全局数据结构维护跨Region的引用关系。 我们来看看Shenandoah的搜集过程。

  4. 初始标记,与G1类似
  5. 并发标记,与G1类似
  6. 最终标记,与G1类似,此阶段统计高回收价值的Region,STW时间较短。
  7. 并发清理,清理无存活对象的region。
  8. 并发回收,与G1不同的核心点,与用户线程并发,将region中存活的对象并发拷贝到其他未使用的region中。Shenandoah通过读屏障和转发指针来解决转移过程中用户线程读写对象的问题。
  9. 初始引用更新,STW时间较短,等待并发回收中所有的收集器线程已经完成工作。
  10. 并发引用更新,更新移动后的对象的指针,指向新的地址,与用户线程并发。
  11. 最终引用更新,修正GC roots中的引用,STW时间较短,与GC roots数量成正比。
  12. 并发清理,清理Region,归还内存。

ZGC垃圾收集器

ZGC收集器是一款基于Region内存布局,暂时不设分代的,使用读屏障、染色指针和内存多重映射等技术来实现可并发标记整理算法的,以低延迟为首要目标的一款垃圾收集器。 内存布局: 小型Region,固定容量为2MB,放置小于256KB的小对象。 中型Region,固定容量32MB,放置大于256KB小于4MB对象。 大小Region,不固定容量,动态变化,但必须是2M的整数倍。放置大于4MB的大对象,一个对象使用一个Region,不会被重分配,即复制到其他Region。 颜色指针:ZGC仅支持64位平台,ZGC将标记设置在指针多余的位上。 其他垃圾收集器 Epsilon收集器,是为隔离垃圾收集器与Java虚拟机解释、编译、监控等其他子系统的接口的一个实现。无垃圾回收功能。 PGC(Azul System公司) C4(Concurrent Continously Compacting Collector)

参考文献

垃圾回收的算法与实现 深入理解Java虚拟机(第3版) 垃圾收集器ParNew和CMS与底层三色标记详解 垃圾收集器G1和ZGC详解 垃圾回收GC3种算法的衍生品 增量回收:预测和控制GC所产生的中断时间 内存分配器