TFdream / jvm-learning

深入拆解Java虚拟机 学习笔记

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

彻底搞懂JDK11前的7个垃圾收集器

TFdream opened this issue · comments

彻底搞懂JDK11前的7个垃圾收集器

上一篇文章 jvm垃圾回收机制 我们了解了 垃圾回收机制和算法等知识点,今天我们来讲讲jvm最重要的堆内存是如何使用垃圾回收器进行垃圾回收,并且如何使用命令去配置使用这些垃圾回收器。

回收哪些区域的对象

需要注意的是,JVM GC只回收堆内存和方法区内的对象。而栈内存的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

垃圾回收器总览

image

新生代可配置的回收器:Serial、ParNew、Parallel Scavenge

老年代配置的回收器:CMS、Serial Old、Parallel Old

新生代和老年代区域的回收器之间进行连线,说明他们之间可以搭配使用。

如何选择合适的垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用(Footprint)、吞吐量(Throughput)和延迟(Latency),但是我们仅仅知道这些,再去了解一下诸多的垃圾收集器,并不能有效合理的选择一款适合我们的垃圾收集器。在网上看到很多的文章描述,都是请根据每一个垃圾收集器的特性和对应的场景来使用。这一句话其实一点也没错,但是对于一个接触垃圾收集器不多的人来说,可能就会造成一脸懵。本文主要整理选择垃圾收集器的方法

从JDK1.8的文档来看,官方显然是很中意G1的,努力的重点也是放在这上面。但是从我个人的使用经验来看,最稳的还是PS+PSOld的组合,ParNew+CMS次之。

垃圾收集器选择场景分析

  • 应用程序需要尽快计算出结果,目标是能尽快计算出结果,那吞吐量就是最主要的点【吞吐量】。
  • 如果是SLA应用,那停顿时间直接影响服务质量,严重的甚至会导致事务超时,关注点就是延迟【延迟】。
  • 如果是客户端或者嵌入式应用,那垃圾收集器的内存占用则是不可忽视的【内存占用】。

Serial/Serial Old 收集器

  • Serial 是一个新生代收集器,基于标记-复制算法实现
  • Serial Old 是一个老年代收集器,基于标记-整理算法实现
  • 两者都是单线程收集,需要「Stop The World」

应用场景

  • 客户端模式下的默认新生代收集器
  • 对于内存资源受限的环境,它是所有收集器里额外内存消耗最小的
  • 对于单核处理器或处理器核心数较少的环境来说,Serial 收集器由于没有线程交互的开销

ParNew 收集器

  • ParNew 收集器是是一款新生代收集器。与Serial收集器相比,支持垃圾收集器多线程并行收集,其余参数和Serial一模一样。俗称:并行垃圾回收器,采用复制算法进行垃圾回收。
  • ParNew默认开启的线程数与CPU数量相同,在CPU核数很多的机器上,可以通过参数-XX:ParallelGCThreads来设置线程数。

Parallel Scavenge/Parallel Old 收集器

  • Parallel Scavenge 收集器是一款新生代收集器,基于标记-复制算法实现
  • Parallel Old 收集器是一款老年代收集器,基于标记-整理算法实现
  • 两者都支持多线程并行收集,需要「Stop The World」
  • 控制吞吐量为目标

特性

  1. 可控制的吞吐量
    两个参数用于精确控制吞吐量,分别是:
    • 控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis ( >0 的毫秒数)
    • 设置吞吐量大小的 -XX:GCTimeRatio(0-100 的整数)
  2. 自适应的调节策略
    -XX:+UseAdaptiveSizePolicy 当这个参数被激活之后,就不需要人工指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。

使用-XX:+UseParallelGC参数可以设置新生代使用这个并行回收器,即 Parallel Scavenge + Parallel Old。

CMS 回收器

CMS全称为:Concurrent Mark Sweep意为并发标记清除,他使用的是标记清除法。主要关注系统停顿时间

  • CMS(Concurrent Mark Sweep)是一个老年代收集器,基于标记-清除算法实现
  • 以最短回收停顿时间为目标

使用-XX:+UseConcMarkSweepGC 进行设置老年代使用该回收器。
使用-XX:ConcGCThreads设置并发线程数量。

实现步骤

  1. 初始标记:标记一下 GC Roots 能直接关联到的对象,速度很快,需要「Stop The World」
  2. 并发标记:从 GC Roots 的直接关联对象开始遍历整个对象图,并发执行
  3. 重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要「Stop The World」
  4. 并发清除:清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,并发执行

缺点

  • 并发阶段占用一部分线程,CMS 默认启动的回收线程数是(处理器核心数量 +3)/4
  • 并发标记和清理阶段,程序可能会有垃圾对象不断产生最终导致 Full GC
  • 为了支持并发标记和清理阶段程序运行,超过参数值 -XX:CMSInitiatingOccupancyFraction 后临时使用 Serial Old 收集器进行一次 Full GC
  • 基于标记-清除算法实现会有大量空间碎片产生,CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整理过程,由于这个内存整理必须移动存活对象,因此无法并发执行。

应用场景

关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。

特性
CMS不会等到应用程序饱和的时候才去回收垃圾,而是在某一阀值的时候开始回收,回收阀值可用指定的参数进行配置:-XX:CMSInitiatingoccupancyFraction来指定,默认为68,也就是说当老年代的空间使用率达到68%的时候,会执行CMS回收。

如果内存使用率增长的很快,在CMS执行的过程中,已经出现了内存不足的情况,此时CMS回收就会失败,虚拟机将启动老年代串行回收器(Serial Old 收集器)进行垃圾回收,这会导致应用程序中断,直到垃圾回收完成后才会正常工作。这个过程GC的停顿时间可能较长,所以-XX:CMSInitiatingoccupancyFraction的设置要根据实际的情况。

之前我们在学习算法的时候说过,标记清除法有个缺点就是存在内存碎片的问题,那么CMS有个参数设置-XX:+UseCMSCompactAtFullCollecion可以使CMS回收完成之后进行一次碎片整理。

-XX:CMSFullGCsBeforeCompaction参数可以设置进行多少次CMS回收之后,对内存进行一次压缩。

G1 回收器

  • G1 是一款主要面向服务端应用的垃圾收集器。
  • 从整体来看是基于「标记-整理」算法实现的收集器,但从局部(两个 Region 之间)上看又是基于「标记-复制」算法实现
  • G1 即是新生代又是老年代收集器”,无需组合其他收集器。

每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待

G1 收集器 Region 划分:

image

特性

  • Region 区域:把连续的 Java 堆划分为多个大小相等的独立 Region,每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间
  • Humongous 区域:专门用来存储大对象。只要大小超过了一个 Region 容量一半的对象即可判定为大对象。
  • 基于停顿时间模型:消耗在垃圾收集上的时间大概率不超过 N 毫秒的目标(使用参数-XX:MaxGCPauseMillis 指定,默认值是 200 毫秒)
  • Mixed GC 模式:可以面向堆内存任何部分来组成「回收集」进行回收,衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大
  • 无内存空间碎片:G1 算法实现意味着运作期间不会产生内存空间碎片

每个 Region 的大小可以通过参数-XX:G1HeapRegionSize 设定,取值范围为 1MB~32MB,且应为 2 的 N 次幂。而对于那些超过了整个 Region 容量的超级大对象,将会被存放在 N 个连续的 Humongous Region 之中,G1 的大多数行为都把 Humongous Region 作为老年代的一部分来进行看待

大致实现步骤

  1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的 Region 中分配新对象。
    需要「Stop The World」,但耗时很短,而且是借用进行 Minor GC 的时候同步完成的。
  2. 并发标记:从 GC Root 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以后,还要重新处理 SATB 记录下的在并发时有引用变动的对象。
  3. 最终标记:对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
  4. 筛选回收:负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。需要「Stop The World」,多条收集器线程并行完成。

TAMS 指针/SATB 记录的概念请阅读《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)》周志明, 3.4.6、3.5.7 内容。

TAMS 指针简单理解为:G1 为每一个 Region 设计了两个名为 TAMS(Top at Mark Start)的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上
SATB 记录简单理解为:解决并发扫描时对象的消失问题

G1 缺点

  • G1 内存占用比 CMS 高,每个 Region 维护一个卡表
  • G1 额外执行负载比 CMS 高,维护卡表的额外操作复杂

G1 应用场景

G1 的首要重点是为运行需要大堆且 GC 延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为 6 GB 或更大,并且稳定且可预测的暂停时间低于 0.5 秒。

如果应用程序具有以下一个或多个特征,那么今天运行 CMS 或并行压缩的应用程序将从切换到 G1 中受益。

  • 超过 50%的 Java 堆被实时数据占用。
  • 对象分配率或提升率差异很大。
  • 应用程序希望暂停时间低于 0.5 秒。

G1 最佳实践

1.不要设置年轻代的大小

如果通过 -Xmn 显式地指定了年轻代的大小, 则会干扰到 G1 收集器的默认行为。

G1 在垃圾收集时将不再关心暂停时间指标。所以从本质上说,设置年轻代的大小将禁用暂停时间目标。
G1 在必要时也不能够增加或者缩小年轻代的空间。 因为大小是固定的,所以对更改大小无能为力。

2.响应时间指标

设置 XX:MaxGCPauseMillis=N 时不应该使用平均响应时间 (ART, average response time) 作为指标,而应该考虑使用目标时间的 90% 或者以上作为响应时间指标。也就是说 90% 的用户请求响应时间不会超过预设的目标值。暂停时间只是一个目标,并不能保证总是满足。

3.什么是转移失败 (Evacuation Failure)?

对 Survivors 或晋升对象进行 GC 时如果 JVM 的 Heap 区不足就会发生提升失败。堆内存不能继续扩充,因为已经达到最大值了。

当使用 -XX:+PrintGCDetails 时将会在 GC 日志中显示 to-space overflow。

这是很昂贵的操作!