Java 捡垃圾利器

文章目录

      • 垃圾收集器
        • Serial
          • 优点
          • 使用场景
        • ParNew
          • 使用场景
          • 配合使用
            • CMS
            • G1
        • Parallel Scavenge
          • 使用场景
          • 参数
        • Serial Old
        • Parallel Old
        • CMS
          • 流程
          • 优点
          • 缺点
        • G1
          • 概述
          • 主要特征
          • 内存分布
          • 回收
          • 定位
          • 问题与解决方案
          • 与 CMS
        • 小结
        • 拓展阅读
        • 参考资料

垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的实践者。

Java 捡垃圾利器_第1张图片

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

在 JDK 8 时将 Serial+CMS 、 ParNew+Serial Old 声明废弃,JDK 9 中完全取消。

  • 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
  • 并行(Parallel):同一时间有多条垃圾收集器线程在协同工作,默认此时的用户线程是处于等待状态
  • 并发(Concurrent):垃圾收集器和用户程序同时执行。会影响吞吐率。除了 CMS 和 G1 之外,其它垃圾收集器都是以并行的方式执行。

Serial

Java 捡垃圾利器_第2张图片

单线程工作的收集器使用复制算法,仅使用一个处理器或一条收集线程去完成垃圾收集工作,在进行垃圾收集时,必须暂停其他所有工作线程,直到收集结束

优点

与其他单线程收集器相比更加的简单高效

  • 内存资源受限的环境中,它是额外内存消耗最小的。
  • 在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
使用场景

所以它是 Client 场景下的默认新生代收集器,因为在该场景下内存一般来说不会很大。它收集一两百兆垃圾的停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿时间是可以接受的。

ParNew

Java 捡垃圾利器_第3张图片

它是 Serial 收集器的多线程版本,使用复制算法。除了同时使用多条线程外,其他的行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与 Serial 完全一致。

使用场景

服务端模式下的虚拟机,JDK 7之前遗留系统中首选的新生代收集器除了Serial收集器外,只有它能与 CMS 收集器配合使用。

在单核处理器中性能比 Serial 差,由于存在线程切换的开销,在超线程中性能也比 Serial 差。

默认开启的线程等于CPU核心数量,使用-XX:ParallelGCThreads 现在垃圾收集的线程数。

配合使用
CMS

CMS 是第一款并发收集器一般针对老年代使用。使用 -XX:+UseConcMarkSweepGC激活。在 JDK 5使用时新生代选择 ParNew 或 Serial 收集器。

ParNew 是CMS的默认新生代收集器,使用-XX:+/-UseParNewGC 指定或禁用。

G1

G1是面向全堆的收集器,不需要配合其他新生代收集器工作。

JDK 9 之后,使用 G1 取代 CMS + ParNew,且 ParNew 已融合进 CMS 中。

Parallel Scavenge

Java 捡垃圾利器_第4张图片

新生代收集器,基于标记-复制算法,能并行收集的多线程收集器。

使用场景

其它收集器目标是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,因此它被称为**“吞吐量优先”**收集器。

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 运行垃圾收集时间)

  • 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。

  • 而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。

参数
  • -XX: MaxGCPauseMillis:允许一个大于 0 的毫秒数,收集器尽力保证内存回收时间不超过设定值。

  • -XX: GCTimeRatio:大于 0 小于 100 的整数,表示垃圾收集时间占总时间的比例,相当于吞吐率的倒数。

  • -XX: +UseAdaptiveSizePolicy:

    • 开启 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX: SurviorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了。
    • 虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
    • 只需要设置基本的内存数据(-Xmx设置最大堆),然后设置虚拟机的优化目标。
    • 自适应调节策略是区别于 ParNew 的重要特性。

Serial Old

Java 捡垃圾利器_第5张图片

Serial 收集器的老年代版本,使用标记-整理算法,也是给 Client 场景下的虚拟机使用。

如果用在 Server 场景下,它有两大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
  • 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。

Parallel Old

Java 捡垃圾利器_第6张图片

Parallel Scavenge 收集器的老年代版本,使用标记-整理算法。

注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。

CMS

Java 捡垃圾利器_第7张图片

CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。针对老年代的以获取最短回收停顿时间为目标的收集器。

流程
  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿
  • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿
  • 并发清除:清理标记好的对象,不需要移动存活对象。不需要停顿

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,总体来说 CMS 内存回收过程是与用户线程一起并发执行的。

优点

并发收集、低停顿。

缺点
  1. 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
  • 占用了一部分线程而导致应用程序变慢,降低总吞吐率。默认启动的线程数是 (处理器核心数量+3) / 4,核心数在4个或以上时,只占用不超过25%,核心越多影响越小。

  • 处理器负载高时,就可能导致用户程序的执行速度忽然大幅降低。

  • “增量式并发收集器(i-CMS)”:在并发标记、清理的时候让收集器线程、用户线程交替运行。

  1. 无法处理浮动垃圾,可能出现 Concurrent Mode Failure,从而导致一次 Full GC。
  • 浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将冻结用户线程的执行,临时启用 Serial Old 来替代 CMS,从“标记-清除”转为“标记-整理”,这样会使得停顿时间加长。
  • JDK 5:当老年代使用了 68% 的空间后就会被激活,JKD 6:阈值默认提升至 92%。
  • -XX: CMSInitiatingOccupancyFraction值来提高CMS的出发百分比,降低内存回收频率,获取更好的性能。设置得太高容易导致大量的并发失败产生,性能反而降低。
  1. 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发另一次 Full GC。
  • -XX:+UseCMS-CompactAtFullCollection:用于在 CMS 收集器不得不进行Full GC时开启内存碎片的合并整理过程,使用一次“标记-整理”算法。此过程需要移动存活对象,所以无法并发。JDK 9开始废弃。
  • -XX:CMSFullGCsBefore-Compaction:执行一定次数不整理空间 Full GC 后,执行一次整理空间的 Full GC,默认为每次都整理。JDK 9开始废弃。
  • 让虚拟机平时使用“标记-清除”算法,但内存碎片化程度已经大到影响内存分配时,使用一次“标记-整理”算法,如 CMS 收集器。
    Java 捡垃圾利器_第8张图片

G1

概述

G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。

堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1是面向局部收集的设计思路和基于 Region 的内存布局形式,可以直接对新生代和老年代一起回收。

主要特征

停顿时间模型

能够支持指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集器上的时间大概率不超过 N 毫秒这样的目标。

使用 Mixed GC 模式支持此模型的实现,面向堆内存任何部分来组成回收集(Collection Set)进行回收,标准为哪块内存中存放的垃圾数量最多,回收收益最大

-XX: MaxGCPauseMillis,默认为 200 毫秒,如果停顿目标时间太短,则可能会因为收集速度跟不上分配速度而导致垃圾堆积最终引发 Full GC。

内存分布

Java 捡垃圾利器_第9张图片

Java 捡垃圾利器_第10张图片

遵循分代收集理论设计,G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。

每一个 Region 都可以是新生代的 Eden 空间、Survivor 空间或者老年代空间,G1 收集器会针对不同角色的 Region 采用不同的收集策略。

Humongous 区域存放大对象,默认为超过了 Region 容量一半的对象即为大对象,该阈值可通过 -XX:G1HeapRegionSize 设定,范围为 1MD ~ 32MB,且为 2 的 N 次幂。

如果对象比 Humongous 还大,则将之存放在连续的 Humongous 中,因为该区域存放的对象很大,频繁回收引起的消耗会很大,所以 Humongous Region 应作为老年代的一部分。

回收

每次收集的内存空间都为 Region 大小的整数倍。

通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集停顿时间,优先回收价值最大的 Region。

Java 捡垃圾利器_第11张图片

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:

  • 初始标记标记 GC Roots 能直接关联到的对象并修改 TAMS 指针的值,使得下一阶段用户线程并发运行时能正确地在可用的 Region 中分配对象。在进行 Minor GC 时同步完成停顿操作耗时较短
  • 并发标记并发操作,进行可达性分析,扫描整个堆里的对象图,耗时较长
  • 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行
  • 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,最后把决定要回收的 Region 的存活对象复制到空的 Region 中,再清理整个旧 Region 的全部空间,涉及对象移动,必须暂停用户线程。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。此阶段占停顿期望值的大部分。
定位

在 JDK 9 时宣告取代 Parallel Scavenge 和 Parallel Old 组合,成为服务端模式下的默认垃圾收集器, CMS 被声明为不推荐使用的收集器。

除了并发标记外,其余阶段都需要完全暂停用户线程,追求的是在延迟可控的情况下获得尽可能高的吞吐量。

问题与解决方案
  • 如何解决跨 Region 引用对象
    1. 每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
      • Key 是 Region 的起始地址, Value 是存储卡表的索引号的集合。
      • 双向卡表:记录“我指向谁”以及“谁指向我”。比传统卡表占用更多内存。
  • 并发标记阶段如何保证收集线程与用户线程互不干扰的运行
    1. 解决用户线程改变对象引用关系时,保证其不能打破原本的对象图结构,导致标记结果出错
      • CMS 采用增量更新算法实现,G1 采用原始快照(SATB)实现。
    2. 回收过程中新创建对象的内存分配
      • 每个 Region 都有两个 TAMS(Top at Mark Start)指针,在 Region 内部有一部分内存用于并发回收过程中的新对象分配,新分配的对象地址都必须要在这两个指针位置以上。这个位置的对象默认存活,即不会被回收。
      • 内存回收速度赶不上内存分配速度时,G1 会冻结用户线程,导致 Full GC 而产生长时间 “Stop The World”。
  • 如何建立可靠的停顿预测模型
    1. 使用 -XX: MaxGCPauseMillis指定停顿的期望值。
    2. 衰减均值
      • 通过 Region 的回收耗时、记忆集里的脏卡数量等可测量的步骤花费的成本,算出平均值、标准偏差、置信度等统计信息。
      • 平均值代表整体平均状态,衰减平均值代表最近的平均状态。统计状态越新越能绝对其回收价值。
      • 通过该值可预测在期望停顿时间下获得的最高收益。
与 CMS
  • 算法:CMS 使用 标记-整理。G1 在局部上因为使用 标记-复制,在整体上使用 标记-整理,G1 运作期间不会产生内存空间碎片,垃圾收集完成之后能提供规整的可用内存。
  • 消耗:G1 垃圾收集产生的内存占用以及程序运行的额外执行负载都比 CMS 高。
    • 内存占用:G1 的卡表实现更为复杂,老年代和新生代都有卡表。CMS 只有老年代有卡表,记录老年代到新生代的引用,但是当 CMS 发生 Old GC 时,要把整个新生代作为 GC Roots 扫描。
    • 执行负载:CMS 使用写后屏障更行维护卡表。G1 除了使用写后屏障来进行同样的操作,为了实现原始快照搜索 SATB 算法,还需要使用写前屏障来跟踪并发时的指针变化情况。
      • 原始快照搜索 STAB:减少并发标记和重新标记阶段的消耗,减少停顿时间,但会因为跟踪引用变化而产生额外负担。
      • 写时屏障:CMS 直接同步。G1为消息队列结果,把写前屏障和写后屏障中要做的事情都放到队列,再进行异步处理。
  • 使用场景:小内存上使用 CMS,大内存上使用 G1,一般的界限为 6GB ~ 8GB。

小结

  • **停顿状态:**浅色为必须挂起用户线程,深色为用户线程与垃圾收集线程并发。

Java 捡垃圾利器_第12张图片

  • 性能指标:内存占用、吞吐量、延迟。随着硬件规格的提升,内存占用和吞吐量不再是瓶颈,延迟才是最需要优化的地方。内存越大,延迟可能就越高,降低延迟就是缩短 Stop The World 的时间。
    • CMS 使用增量更新,G1 使用原始快照的方式实现标记阶段的并发,不会因为堆内存变大而导致停顿时间增长。
    • CMS 使用标记-清除算法,会积累内存碎片,碎片到达一定量的时候会引发一次 Full GC,这个过程会停顿用户线程。
    • G1 的回收粒度更小,所需要整理的内存就越少,所停顿的时间就越短。

拓展阅读

Java 捡垃圾黑科技

关于 Java 捡垃圾那些事


⭐️ 如果对你有帮助,请点个赞

参考资料

《深入理解Java虚拟机-第三版》
cyc 2018
JavaGuide
《Java虚拟机原理图解》4.JVM机器指令集

你可能感兴趣的:(JVM)