四、JVM 垃圾收集算法 和 垃圾收集器


一、垃圾收集算法

在这里插入图片描述


1. 标记-清除算法

  • 算法分为 “标记” 和 "清除“ 阶段。
  1. 首先标记出所有需要回收的对象。
  2. 在标记完成后,统一回收所有被标记的对象。
  • 它是最基础的收集算法,效率也很高
  • 但是会带来两个明显的问题。
    效率问题、空间问题(标记清除后会产生大量不连续的碎片)。
    在这里插入图片描述

2. 复制算法

  • 为了解决效率问题,“复制” 收集算法出现了。
  1. 它可以将内存分为大小相同的两块,每次使用其中的一块。
  2. 当这一块的内存使用完后,就将还存活的对象复制到另一块去。
  3. 然后再把使用的空间一次清理掉。
  • 这样就使 每次的内存回收,都是对内存区间的一半 进行回收
    在这里插入图片描述

3. 标记-整理算法

  • 根据老年代的特点,推出的一种 标记算法。
  1. 标记过程仍然与 “标记-清除” 算法一样。
  2. 但后续步骤,不是直接对 可回收对象回收。
  3. 而是让所有存活的对象向一端移动。
  4. 然后直接清理掉边界以外的内存。
    在这里插入图片描述

4. 分代收集算法

  • 当前虚拟机 的 垃圾收集 都采用 分代收集算法
  1. 这种算法没有什么新的思想,只是根据 对象存活周期 的不同将内存分为几块。
  2. 一般将 Java 堆分为 新生代 和 老年代。
  3. 这样我们就可以根据各个年代的特点,选择合适的 垃圾收集算法。

  • 比如:在新生代中,每次收集都会有大量对象(近 99%)死去。
  1. 所以可以选择 复制算法
  2. 只需要付出少量对象的复制成本,就可以完成每次垃圾收集。

  • 而 老年代 的对象存活几率是比较高的,而且没有额外的空间 对它进行分配担保。
  1. 所以我们必须选择 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。
  • 注意:“标记-清除” 或 “标记-整理” 算法,会比 复制算法 慢 10 倍以上

二、垃圾收集器

在这里插入图片描述

  • 如果说 收集算法 是内存回收的 方法论,那么 垃圾收集器 就是内存回收的 具体实现
  • 虽然我们对 各个收集器 进行比较,但并非为了挑选出一个最好的收集器。
  • 因为直到现在为止,还没有最好的垃圾收集器出现,更加没有 万能的垃圾收集器。
  • 我们能做的就是,根据具体应用场景,选择适合自己的垃圾收集器

  • 如果有一种四海之内、任何场景下都适用的完美收集器存在。
  • 那么我们的 Java 虚拟机,就不会实现那么多不同的 垃圾收集器了。

1. Serial 收集器

  • Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。
  • 这个收集器是一个单线程收集器了。
  1. 它的 “单线程” 的意义,不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作。
  2. 更重要的是它在 进行垃圾收集工作 的时候,必须暂停其他所有的工作线程(Stop The World),直到它收集结束。

  • 虚拟机的设计者们当然知道(Stop The World)带来的不良用户体验。
  • 所以在后续的垃圾收集器设计中,停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
# 年轻代使用`Serial`收集器
-XX:+UseSerialGC	
# 老轻代使用`Serial Old`收集器
-XX:+UseSerialOldGC

  • 新生代采用 复制算法,老年代采用 标记-整理算法
    在这里插入图片描述
  • Serial 收集器,优于其他垃圾收集器的地方。
  1. 简单而高效
  2. 与其他收集器的单线程相比,Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
  • Serial Old 收集器是 Serial 收集器的老年代版本
    它同样是一个单线程收集器。

  • Serial 收集器,主要有两大用途。
  1. 一种用途是在 JDK-1.5 以及以前的版本中,与 Parallel Scavenge 收集器搭配使用。
  2. 另一种用途是作为 CMS 收集器的后备方案

2. ParNew 收集器

  • ParNew 收集器,其实就是 Serial 收集器的 多线程 版本
  1. 除了使用多线程进行垃圾收集外。
    其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
  2. 默认的收集 线程数 跟 CPU 核数相同
    当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。
# 使用`ParNew`收集器
-XX:+UseParNewGC

  • 新生代采用 复制算法,老年代采用 标记-整理算法
    在这里插入图片描述
  • 它是许多运行在 Server 模式下的虚拟机的首要选择。
  • 除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器)配合工作。

3. Parallel Scavenge 收集器

  • Parallel Scavenge 收集器类似于 ParNew 收集器。
  • Parallel ScavengeServer 模式(内存大于 2G2个CPU)下的默认收集器
  1. 收集器关注点是吞吐量(高效率的利用 CPU)。
    所谓吞吐量就是 CPU 中用于运行用户代码的时间 与 CPU 总消耗时间的比值。
  2. CMS 等垃圾收集器的关注点,更多的是用户线程的停顿时间(提高用户体验)。

  • Parallel Scavenge 收集器提供了很多参数。
    供用户找到最合适的 停顿时间 或 最大吞吐量。
  • 如果对于 收集器运作 不太了解的话。
    可以选择把内存管理优化,交给虚拟机去完成也是一个不错的选择。
# 年轻代
-XX:+UseParallelGC
# 老年代
-XX:+UseParallelOldGC

  • 新生代采用 复制算法,老年代采用 标记-整理算法
    在这里插入图片描述
  • Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本。
    使用 多线程和 “标记-整理” 算法。
  • 在注重吞吐量以及 CPU 资源的场合。
    都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

4. CMS 收集器

  • CMS(Concurrent Mark Sweep 并发标记)收集器。
    是一种以获取 最短回收停顿时间 为目标的收集器。
  1. 它非常符合在注重用户体验的应用上使用。
  2. 它是 HotSpot 虚拟机第一款真正意义上的 并发收集器
  3. 它第一次实现了让 垃圾收集线程 与 用户线程(基本上)同时工作。
# Old
-XX:+UseConcMarkSweepGC

  • 从名字中的 Mark Sweep 这两个词可以看出。
  1. CMS 收集器是一种 “标记-清除” 算法实现的。
  2. 它的运作过程,相比于前面几种垃圾收集器来说更加复杂一些。

在这里插入图片描述

  • 整个过程分为四个步骤。
  1. 初始标记
  1. 暂停所有的其他线程。
  2. 并记录下 GCRoots 直接能引用的对象,速度很快。
  1. 并发标记
  1. 同时开启 GC 和 用户 线程,用一个闭包结构去记录可达对象。
  2. 但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。
  3. 因为用户线程可能会不断的更新引用域。所以 GC 线程无法保证可达性分析的实时性。
  4. 所以这个算法里,会跟踪记录这些发生引用更新的地方。
  1. 重新标记
  1. 重新标记阶段就是为了修正并发标记期间,因为用户程序继续运行,而导致标记产生变动的那一部分对象的标记记录。
  2. 这个阶段的停顿时间,一般会比 初始标记 阶段的时间稍长,远远比 并发标记 阶段时间短。
  1. 并发清理
    开启 用户线程,同时 GC 线程 开始对未标记的区域做清扫。

4.1 CMS 优缺点
  • CMS 的优点。
  1. 并发收集
  2. 低停顿

  • CMS 的缺点。
  1. 对 CPU 资源敏感(会和服务抢资源)。
  2. 无法处理 浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次 GC 再清理了)。
  3. 它使用的回收算法 “标记-清除” 算法,会导致收集结束时会有大量空间碎片产生。
    当然通过参数(-XX:+UseCMSCompactAtFullCollection)可以让 JVM 在执行完标记清除后再做整理。
  4. 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况。
  1. 特别是在 并发标记 和 并发清理 阶段会出现,一边回收,系统一边运行。
  2. 也许没回收完就再次触发 FullGC,也就是(“Concurrent Mode Failure”)。
  3. 此时会进入(Stop The World),用 Serial old 垃圾收集器来回收

4.2 CMS 相关参数
# 启用`CMS`。
-XX:+UseConcMarkSweepGC

# 并发的`GC`线程数。
-XX:ConcGCThreads

# `FullGC`之后做压缩整理(减少碎片)。
-XX:+UseCMSCompactAtFullCollection

# 多少次`FullGC`之后压缩一次(默认:`0`),代表每次`FullGC`后都会压缩一次。
-XX:CMSFullGCsBeforeCompaction

# 当老年代使用达到该比例时会触发`FullGC`(默认:`92`这是百分比)。
-XX:CMSInitiatingOccupancyFraction

# 只使用设定的回收阈值(`-XX:CMSInitiatingOccupancyFraction`设定的值)。
# 如果不指定,`JVM`仅在第一次使用设定值,后续则会自动调整。
-XX:+UseCMSInitiatingOccupancyOnly

# 在`CMS GC`前启动一次`MinorGC`,目的在于减少老年代对年轻代的引用,降低`CMS GC`的标记阶段时的开销。
# 一般`CMS`的`GC`耗时`80%`都在`Remark`阶段。
-XX:+CMSScavengeBeforeRemark

4.3 亿级流量,优化 JVM 参数配置(ParNew + CMS)

5. G1 收集器

  • G1(Garbage-First 垃圾首先)是一款面向服务器的垃圾收集器。
  1. 主要针对配备 多颗处理器 及 大容量内存 的机器
  2. 以极高概率满足 GC 停顿时间 要求的同时,还具备 高吞吐量 性能特征。
-XX:+UseG1GC

在这里插入图片描述

  • G1 将 Java 堆划分为多个大小相等的独立区域(Region,JVM 最多可以有 2048 个 Region)。
  • 一般 Region 大小 等于 堆大小 除以 2048(比如:堆大小为 4096M,则 Region 大小为 2M)。
    当然也可以用参数(-XX:G1HeapRegionSize)手动指定 Region 大小。
    但是推荐默认的计算方式。
  • G1 保留了 年轻代 和 老年代 的概念,但不再是物理隔阂了
    它们都是(可以不连续)Region 的集合。

5.1 G1 的年轻代划分
  • 默认 年轻代 对 堆内存 的占比是 5%
    如果堆大小为 4096M,那么 年轻代 占据 200MB 左右的内存。
  1. 对应大概是 100 个 Region。
    可以通过(-XX:G1NewSizePercent)设置 新生代 初始占比。
  2. 在系统运行中,JVM 会不停的给 年轻代 增加更多的 Region。
  3. 但是最多 新生代 的占比不会超过 60%
    可以通过(-XX:G1MaxNewSizePercent)调整。

  • 年轻代 中的 EdenSurvivor 对应的 Region 也跟之前一样(默认:8:1:1)。
  1. 假设 年轻代 现在有 1000 个 Region。
  2. Eden 区对应 800 个。
  3. S0 对应 100个。
  4. S1 对应 100 个。

  • 一个 Region 可能之前是 年轻代。
  1. 如果 Region 进行了垃圾回收,之后可能又会变成 老年代。
  2. 也就是说 Region 的区域功能可能会动态变化。

5.2 G1 分配大对象的 Humongous
  • G1 垃圾收集器,对于对象什么时候会转移到 老年代 跟之前的原则一样。
  1. 唯一不同的是对 大对象 的处理。
    G1 有专门分配大对象的 Region 叫 Humongous 区。
  2. 而不是让大对象直接进入 老年代 的 Region 中。
  • G1 中大对象的判定规则。
    就是一个 大对象 超过了一个 Region 大小的 50%

  • 比如:按照上面算的,每个 Region 是 2M。
  1. 只要一个大对象超过了 1M,就会被放入 Humongous 中。
  2. 而且一个大对象如果太大,可能会横跨多个 Region 来存放。

  • Humongous 区专门存放短期巨型对象,不用直接进老年代。
  • 可以节约老年代的空间,避免因为老年代空间不够的 GC 开销。

  • FullGC 的时候,除了收集 年轻代 和 老年代 之外,也会将 Humongous 区一并回收。

5.3 G1 收集过程

在这里插入图片描述

  • G1 收集器一次,GC 的运作过程大致分为以下几个步骤。
  1. 初始标记(Initial mark,STW)。
    暂停所有的其他线程,并记录下 GCRoots 直接能引用的对象,速度很快。
  2. 并发标记(Concurrent Marking)。
    CMS 的并发标记。
  3. 最终标记(Remark,STW)。
    CMS 的重新标记。
  4. 筛选回收(Cleanup,STW)。
  1. 筛选回收阶段,首先对各个 Region 的 回收价值 和 成本 进行排序
    根据 用户 所期望的 GC 停顿时间(可以用 JVM 参数 -XX:MaxGCPauseMillis 调节)来制定回收计划。
  2. 比如说 老年代 此时有 1000 个 Region 都满了。
    但是因为根据 预期停顿时间,本次垃圾回收只能停顿 200 毫秒。
  3. 那么通过之前 回收成本 计算得知。
    可能回收其中 800 个 Region 刚好需要 200ms。
    那么就只会回收 800 个 Region。
    尽量把 GC 导致的 停顿时间 控制在我们指定的范围内。
  4. 这个阶段,其实也可以做到与 用户程序 一起并发执行。
    但是因为只回收一部分 Region,时间是用户可控制的。
    而且 停顿用户线程 将大幅提高收集效率
  5. 不管是 年轻代 或是 老年代,回收算法主要用的是 复制算法
    将一个 Region 中的存活对象,复制到另一个 Region 中。
  6. 这种不会像 CMS 那样回收完,因为有很多 内存碎片 还需要整理一次。
    G1 采用 复制算法 回收,几乎不会有太多内存碎片。

  • G1 收集器在后台,维护了一个优先列表。
  1. 每次根据允许的收集时间。
    优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。
  2. 比如一个 Region 花 200ms 能回收 10M 垃圾。
    另外一个 Region 花 50ms 能回收 20M 垃圾。
    在回收时间有限情况下,G1 当然会优先选择后面这个 Region 回收。
  3. 这种使用 Region 划分内存空间,以及有优先级的区域回收方式。
    保证了 G1 收集器,在有限时间内可以尽可能高的收集效率。

5.4 G1 特点
  • G1 被视为 JDK-1.7 以上版本,Java 虚拟机的一个重要进化特征。

  • G1 具备以下特点。
  1. 并行与并发
    G1 能充分利用 CPU、多核环境下的硬件优势。
  1. 使用多个 CPU(CPU 或者 CPU核心)来缩短(Stop-The-World)停顿时间。
  2. 部分其他收集器,原本需要停顿 Java 线程 来执行 GC 动作。
  3. G1 收集器,仍然可以通过 并发的方式 让 Java 程序继续执行。
  1. 分代收集
    虽然 G1 可以不需要其他收集器配合,就能独立管理整个 GC 堆。
    但是还是保留了分代的概念。
  2. 空间整合
    CMS“标记–清理” 算法不同。
  1. G1 从整体来看是基于 “标记-整理” 算法实现的收集器。
  2. 从局部上来看是基于 ”复制“* 算法实现的。
  1. 可预测的停顿
    这是 G1 相对于 CMS 的另一个大优势。
  1. 降低停顿时间是 G1CMS 共同的关注点。
  2. G1 除了追求低停顿外,还能建立 可预测的停顿时间 模型。
  3. 能让使用者明确指定在一个长度为 M 毫秒 的时间片段(通过参数 -XX:MaxGCPauseMillis 指定)内完成垃圾收集。

5.5 G1 相关参数
# 使用`G1`收集器。
-XX:+UseG1GC

# 指定`GC`工作的线程数量。
-XX:ParallelGCThreads

# 指定分区大小(1MB~32MB,且必须是`2`的幂,默认将整堆划分为`2048`个分区)。
-XX:G1HeapRegionSize

# 目标暂停时间(默认:200ms)。
-XX:MaxGCPauseMillis

# 新生代内存初始空间(默认:整堆`5%`)。
-XX:G1NewSizePercent

# 新生代内存最大空间。
-XX:G1MaxNewSizePercent

# `Survivor`区的填充容量(默认:50%)。
# `Survivor 区域里的一批对象(年龄1 + 年龄2 + 年龄n 的多个年龄对象)总和超过了`Survivor`区域的`50%`,此时就会把 年龄n(含)以上的对象都放入老年代。
-XX:TargetSurvivorRatio

# 最大年龄阈值(默认:15)。
-XX:MaxTenuringThreshold

# 老年代占用空间达到整堆内存阈值(默认:45%),则执行新生代和老年代的混合收集(MixedGC)。
# 比如之前说的堆默认有`2048`个`Region`,如果有接近`1000`个`Region`都是老年代的`Region`,则可能就要触发`MixedGC`了。
-XX:InitiatingHeapOccupancyPercent

# `GC`过程中,空出来的`Region`是否充足阈值。
# 1. 在混合回收的时候,对`Region`回收都是基于 复制 算法进行的。
# 2. 都是把要回收的` Region`里的 存活对象 放入其他`Region`,然后这个`Region`中的 垃圾对象 全部清理掉。
# 3. 这样的话在回收过程就会不断空出来新的 `Region`。
# 4. 一旦空闲出来的`Region`数量达到了堆内存的`5%`,此时就会立即停止 混合回收,意味着本次混合回收就结束了。
-XX:G1HeapWastePercent #(默认:5%)

# `Region`中的存活对象低于这个值时才会回收该`Region`,如果超过这个值,存活对象过多,回收的的意义不大。
-XX:G1MixedGCLiveThresholdPercent #(默认:85%)

# 在一次回收过程中指定做几次筛选回收(默认:8次)。
# 在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
-XX:G1MixedGCCountTarget

5.6 G1 垃圾收集分类
  • YoungGC
  1. YoungGC 并不是说现有的 Eden 区放满了就会马上触发。
    而且 G1 会计算下现在 Eden 区回收大概要多久时间。
  2. 如果 回收时间 远远小于参数(-XX:MaxGCPauseMills最大暂停时间)设定的值。
    那么 增加年轻代 的 Region,继续给新对象存放,不会马上做 YoungGC。
  3. 直到下一次 Eden 区放满。
    G1 计算回收时间,接近参数(-XX:MaxGCPauseMills最大暂停时间)设定的值。
    那么就会触发 YoungGC。

  • MixedGC:不是 FullGC
    老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen)设定的值则触发。
  1. 回收所有的 Young 和 部分Old(根据期望的 GC停顿时间,确定 Old区垃圾 收集的优先顺序)以及大对象区。
  2. 正常情况 G1的垃圾收集 是先做 MixedGC,主要使用 复制算法。
  3. 需要把各个 Region 中存活的对象,拷贝到别的 Region 里去。
    拷贝过程中,如果发现没有足够的空 Region 能够承载拷贝对象,就会触发一次 FullGC

  • FullGC:停止系统程序。
  1. 然后采用单线程进行 标记、清理和压缩整理。
  2. 好空闲出来一批 Region 来供下一次 MixedGC 使用。
  3. 这个过程是非常耗时的。

5.7 G1 垃圾收集器优化建议
  • 假设参数(-XX:MaxGCPauseMills最大暂停时间)设置的值很大,导致系统运行很久。
    年轻代 可能都占用了 堆内存 的 60% 了,此时才触发 年轻代GC。
  • 那么存活下来的对象可能就会很多。
    此时就会导致 Survivor区域 放不下那么多的对象,就会进入 老年代 中。
  • 或者是 年轻代GC 过后,存活下来的对象过多。
    导致进入 Survivor区域 后触发了 动态年龄判定 规则。
    达到了 Survivor区域 的 50%,也会快速导致一些对象进入老年代中。
  • 所以这里核心还是在于调节(-XX:MaxGCPauseMills最大暂停时间)这个参数的值。
  1. 在保证 年轻代GC 别太频繁的同时,还得考虑每次 GC 过后的存活对象有多少。
  2. 避免 存活对象 太多快速进入 老年代,频繁触发 MixedGC

5.8 每秒几十万并发,优化 JVM
  • Kafka 类似的支撑高并发消息系统。
  1. 对于 Kafka 来说,每秒处理 几万 甚至 几十万 消息时很正常的。
    一般来说部署 Kafka 需要用大内存机器。
  2. 比如:64G,也就是说可以给 年轻代 分配个 30-40G 的内存,用来支撑高并发处理。

  • 这里就涉及到一个问题了,以前常说的对于 Eden 区的 YoungGC 是很快的。
    这种情况下它的执行还会很快吗?
  1. 很显然,不可能,因为内存太大,处理还是要花不少时间的。
    假设 30-40G 内存回收可能最快也要几秒钟。
  2. 按 Kafka 这个并发量,放满 30-40G 的 Eden 区可能也就一两分钟吧。
  3. 那么意味着整个系统每运行一两分钟。
    就会因为 YoungGC 卡顿几秒钟没法处理新消息,显然是不行的。
  • 那么对于这种情况如何优化了。
  1. 我们可以使用 G1收集器,设置(-XX:MaxGCPauseMills 为 50ms)。
  2. 假设 50ms 能够回收 30-40G 内存。
    然后 50ms 的卡顿其实完全能够接受,用户几乎无感知。
    那么整个系统,就可以在卡顿几乎无感知的情况下,一边处理业务一边收集垃圾。

  • G1 天生就适合这种 大内存 机器的 JVM 运行。
    可以比较完美的解决 大内存 垃圾回收时间过长 的问题。

三、如何选择垃圾收集器

  1. 优先 调整堆的大小,让服务器自己来选择。
  2. 如果内存小于 100M,使用 串行收集器
  3. 如果是 单核,并且没有 停顿时间 的要求,串行 或 JVM 自己选择
  4. 如果允许 停顿时间 超过 1秒,选择 并行 或 JVM 自己选
  5. 如果 响应时间 最重要,并且不能超过 1秒,使用 并发收集器

  • 下图有连线的可以搭配使用,官方推荐使用 G1,因为性能高。
    在这里插入图片描述
    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑士梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值