一、垃圾收集算法
1. 标记-清除算法
- 算法分为 “标记” 和 "清除“ 阶段。
- 首先标记出所有需要回收的对象。
- 在标记完成后,统一回收所有被标记的对象。
- 它是最基础的收集算法,效率也很高。
- 但是会带来两个明显的问题。
效率问题、空间问题(标记清除后会产生大量不连续的碎片)。
2. 复制算法
- 为了解决效率问题,“复制” 收集算法出现了。
- 它可以将内存分为大小相同的两块,每次使用其中的一块。
- 当这一块的内存使用完后,就将还存活的对象复制到另一块去。
- 然后再把使用的空间一次清理掉。
- 这样就使 每次的内存回收,都是对内存区间的一半 进行回收。
3. 标记-整理算法
- 根据老年代的特点,推出的一种 标记算法。
- 标记过程仍然与 “标记-清除” 算法一样。
- 但后续步骤,不是直接对 可回收对象回收。
- 而是让所有存活的对象向一端移动。
- 然后直接清理掉边界以外的内存。
4. 分代收集算法
- 当前虚拟机 的 垃圾收集 都采用 分代收集算法。
- 这种算法没有什么新的思想,只是根据 对象存活周期 的不同将内存分为几块。
- 一般将 Java 堆分为 新生代 和 老年代。
- 这样我们就可以根据各个年代的特点,选择合适的 垃圾收集算法。
- 比如:在新生代中,每次收集都会有大量对象(近 99%)死去。
- 所以可以选择 复制算法。
- 只需要付出少量对象的复制成本,就可以完成每次垃圾收集。
- 而 老年代 的对象存活几率是比较高的,而且没有额外的空间 对它进行分配担保。
- 所以我们必须选择 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。
- 注意:“标记-清除” 或 “标记-整理” 算法,会比 复制算法 慢
10
倍以上。
二、垃圾收集器
- 如果说 收集算法 是内存回收的 方法论,那么 垃圾收集器 就是内存回收的 具体实现。
- 虽然我们对 各个收集器 进行比较,但并非为了挑选出一个最好的收集器。
- 因为直到现在为止,还没有最好的垃圾收集器出现,更加没有 万能的垃圾收集器。
- 我们能做的就是,根据具体应用场景,选择适合自己的垃圾收集器。
- 如果有一种四海之内、任何场景下都适用的完美收集器存在。
- 那么我们的 Java 虚拟机,就不会实现那么多不同的 垃圾收集器了。
1. Serial
收集器
Serial
(串行)收集器是最基本、历史最悠久的垃圾收集器了。- 这个收集器是一个单线程收集器了。
- 它的 “单线程” 的意义,不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作。
- 更重要的是它在 进行垃圾收集工作 的时候,必须暂停其他所有的工作线程(
Stop The World
),直到它收集结束。
- 虚拟机的设计者们当然知道(
Stop The World
)带来的不良用户体验。- 所以在后续的垃圾收集器设计中,停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
# 年轻代使用`Serial`收集器
-XX:+UseSerialGC
# 老轻代使用`Serial Old`收集器
-XX:+UseSerialOldGC
- 新生代采用 复制算法,老年代采用 标记-整理算法。
Serial
收集器,优于其他垃圾收集器的地方。
- 简单而高效。
- 与其他收集器的单线程相比,
Serial
收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
Serial Old
收集器是Serial
收集器的老年代版本。
它同样是一个单线程收集器。
Serial
收集器,主要有两大用途。
- 一种用途是在 JDK-1.5 以及以前的版本中,与
Parallel Scavenge
收集器搭配使用。- 另一种用途是作为
CMS
收集器的后备方案。
2. ParNew
收集器
ParNew
收集器,其实就是Serial
收集器的 多线程 版本。
- 除了使用多线程进行垃圾收集外。
其余行为(控制参数、收集算法、回收策略等等)和Serial
收集器完全一样。- 默认的收集 线程数 跟 CPU 核数相同。
当然也可以用参数(-XX:ParallelGCThreads
)指定收集线程数,但是一般不推荐修改。
# 使用`ParNew`收集器
-XX:+UseParNewGC
- 新生代采用 复制算法,老年代采用 标记-整理算法。
- 它是许多运行在
Server
模式下的虚拟机的首要选择。- 除了
Serial
收集器外,只有它能与CMS
收集器(真正意义上的并发收集器)配合工作。
3. Parallel Scavenge
收集器
Parallel Scavenge
收集器类似于ParNew
收集器。Parallel Scavenge
是Server
模式(内存大于2G
,2个CPU
)下的默认收集器。
- 收集器关注点是吞吐量(高效率的利用 CPU)。
所谓吞吐量就是 CPU 中用于运行用户代码的时间 与 CPU 总消耗时间的比值。CMS
等垃圾收集器的关注点,更多的是用户线程的停顿时间(提高用户体验)。
Parallel Scavenge
收集器提供了很多参数。
供用户找到最合适的 停顿时间 或 最大吞吐量。- 如果对于 收集器运作 不太了解的话。
可以选择把内存管理优化,交给虚拟机去完成也是一个不错的选择。
# 年轻代
-XX:+UseParallelGC
# 老年代
-XX:+UseParallelOldGC
- 新生代采用 复制算法,老年代采用 标记-整理算法。
Parallel Old
收集器是Parallel Scavenge
收集器的老年代版本。
使用 多线程和 “标记-整理” 算法。- 在注重吞吐量以及 CPU 资源的场合。
都可以优先考虑Parallel Scavenge
收集器和Parallel Old
收集器。
4. CMS
收集器
CMS
(Concurrent Mark Sweep 并发标记)收集器。
是一种以获取 最短回收停顿时间 为目标的收集器。
- 它非常符合在注重用户体验的应用上使用。
- 它是
HotSpot
虚拟机第一款真正意义上的 并发收集器。- 它第一次实现了让 垃圾收集线程 与 用户线程(基本上)同时工作。
# Old
-XX:+UseConcMarkSweepGC
- 从名字中的
Mark Sweep
这两个词可以看出。
CMS
收集器是一种 “标记-清除” 算法实现的。- 它的运作过程,相比于前面几种垃圾收集器来说更加复杂一些。
- 整个过程分为四个步骤。
- 初始标记。
- 暂停所有的其他线程。
- 并记录下
GCRoots
直接能引用的对象,速度很快。
- 并发标记。
- 同时开启
GC
和 用户 线程,用一个闭包结构去记录可达对象。- 但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。
- 因为用户线程可能会不断的更新引用域。所以
GC
线程无法保证可达性分析的实时性。- 所以这个算法里,会跟踪记录这些发生引用更新的地方。
- 重新标记。
- 重新标记阶段就是为了修正并发标记期间,因为用户程序继续运行,而导致标记产生变动的那一部分对象的标记记录。
- 这个阶段的停顿时间,一般会比 初始标记 阶段的时间稍长,远远比 并发标记 阶段时间短。
- 并发清理。
开启 用户线程,同时GC
线程 开始对未标记的区域做清扫。
4.1 CMS
优缺点
CMS
的优点。
- 并发收集。
- 低停顿。
CMS
的缺点。
- 对 CPU 资源敏感(会和服务抢资源)。
- 无法处理 浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次
GC
再清理了)。- 它使用的回收算法 “标记-清除” 算法,会导致收集结束时会有大量空间碎片产生。
当然通过参数(-XX:+UseCMSCompactAtFullCollection
)可以让 JVM 在执行完标记清除后再做整理。- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况。
- 特别是在 并发标记 和 并发清理 阶段会出现,一边回收,系统一边运行。
- 也许没回收完就再次触发
FullGC
,也就是(“Concurrent Mode Failure”)。- 此时会进入(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 垃圾首先)是一款面向服务器的垃圾收集器。
- 主要针对配备 多颗处理器 及 大容量内存 的机器。
- 以极高概率满足
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 左右的内存。
- 对应大概是 100 个 Region。
可以通过(-XX:G1NewSizePercent
)设置 新生代 初始占比。- 在系统运行中,JVM 会不停的给 年轻代 增加更多的 Region。
- 但是最多 新生代 的占比不会超过
60%
。
可以通过(-XX:G1MaxNewSizePercent
)调整。
- 年轻代 中的
Eden
和Survivor
对应的 Region 也跟之前一样(默认:8:1:1)。
- 假设 年轻代 现在有 1000 个 Region。
- Eden 区对应 800 个。
- S0 对应 100个。
- S1 对应 100 个。
- 一个 Region 可能之前是 年轻代。
- 如果 Region 进行了垃圾回收,之后可能又会变成 老年代。
- 也就是说 Region 的区域功能可能会动态变化。
5.2 G1
分配大对象的 Humongous
区
G1
垃圾收集器,对于对象什么时候会转移到 老年代 跟之前的原则一样。
- 唯一不同的是对 大对象 的处理。
G1
有专门分配大对象的 Region 叫Humongous
区。- 而不是让大对象直接进入 老年代 的 Region 中。
- 在
G1
中大对象的判定规则。
就是一个 大对象 超过了一个 Region 大小的50%
。
- 比如:按照上面算的,每个 Region 是 2M。
- 只要一个大对象超过了 1M,就会被放入 Humongous 中。
- 而且一个大对象如果太大,可能会横跨多个 Region 来存放。
Humongous
区专门存放短期巨型对象,不用直接进老年代。- 可以节约老年代的空间,避免因为老年代空间不够的
GC
开销。
FullGC
的时候,除了收集 年轻代 和 老年代 之外,也会将Humongous
区一并回收。
5.3 G1
收集过程
G1
收集器一次,GC
的运作过程大致分为以下几个步骤。
- 初始标记(Initial mark,STW)。
暂停所有的其他线程,并记录下GCRoots
直接能引用的对象,速度很快。- 并发标记(Concurrent Marking)。
同CMS
的并发标记。- 最终标记(Remark,STW)。
同CMS
的重新标记。- 筛选回收(Cleanup,STW)。
- 筛选回收阶段,首先对各个 Region 的 回收价值 和 成本 进行排序。
根据 用户 所期望的GC
停顿时间(可以用 JVM 参数-XX:MaxGCPauseMillis
调节)来制定回收计划。- 比如说 老年代 此时有 1000 个 Region 都满了。
但是因为根据 预期停顿时间,本次垃圾回收只能停顿 200 毫秒。- 那么通过之前 回收成本 计算得知。
可能回收其中 800 个 Region 刚好需要 200ms。
那么就只会回收 800 个 Region。
尽量把GC
导致的 停顿时间 控制在我们指定的范围内。- 这个阶段,其实也可以做到与 用户程序 一起并发执行。
但是因为只回收一部分 Region,时间是用户可控制的。
而且 停顿用户线程 将大幅提高收集效率。- 不管是 年轻代 或是 老年代,回收算法主要用的是 复制算法。
将一个 Region 中的存活对象,复制到另一个 Region 中。- 这种不会像
CMS
那样回收完,因为有很多 内存碎片 还需要整理一次。
G1
采用 复制算法 回收,几乎不会有太多内存碎片。
G1
收集器在后台,维护了一个优先列表。
- 每次根据允许的收集时间。
优先选择回收价值最大的 Region(这也就是它的名字Garbage-First
的由来)。- 比如一个 Region 花 200ms 能回收 10M 垃圾。
另外一个 Region 花 50ms 能回收 20M 垃圾。
在回收时间有限情况下,G1
当然会优先选择后面这个 Region 回收。- 这种使用 Region 划分内存空间,以及有优先级的区域回收方式。
保证了G1
收集器,在有限时间内可以尽可能高的收集效率。
5.4 G1
特点
G1
被视为 JDK-1.7 以上版本,Java 虚拟机的一个重要进化特征。
G1
具备以下特点。
- 并行与并发。
G1
能充分利用 CPU、多核环境下的硬件优势。
- 使用多个 CPU(CPU 或者 CPU核心)来缩短(Stop-The-World)停顿时间。
- 部分其他收集器,原本需要停顿 Java 线程 来执行 GC 动作。
- G1 收集器,仍然可以通过 并发的方式 让 Java 程序继续执行。
- 分代收集。
虽然G1
可以不需要其他收集器配合,就能独立管理整个 GC 堆。
但是还是保留了分代的概念。- 空间整合。
与CMS
的 “标记–清理” 算法不同。
G1
从整体来看是基于 “标记-整理” 算法实现的收集器。- 从局部上来看是基于 ”复制“* 算法实现的。
- 可预测的停顿。
这是G1
相对于CMS
的另一个大优势。
- 降低停顿时间是
G1
和CMS
共同的关注点。- 但
G1
除了追求低停顿外,还能建立 可预测的停顿时间 模型。- 能让使用者明确指定在一个长度为 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
。
- YoungGC 并不是说现有的 Eden 区放满了就会马上触发。
而且 G1 会计算下现在 Eden 区回收大概要多久时间。- 如果 回收时间 远远小于参数(
-XX:MaxGCPauseMills
最大暂停时间)设定的值。
那么 增加年轻代 的 Region,继续给新对象存放,不会马上做 YoungGC。- 直到下一次 Eden 区放满。
G1 计算回收时间,接近参数(-XX:MaxGCPauseMills
最大暂停时间)设定的值。
那么就会触发 YoungGC。
MixedGC
:不是FullGC
。
老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercen
)设定的值则触发。
- 回收所有的 Young 和 部分Old(根据期望的 GC停顿时间,确定 Old区垃圾 收集的优先顺序)以及大对象区。
- 正常情况 G1的垃圾收集 是先做
MixedGC
,主要使用 复制算法。- 需要把各个 Region 中存活的对象,拷贝到别的 Region 里去。
拷贝过程中,如果发现没有足够的空 Region 能够承载拷贝对象,就会触发一次FullGC
。
FullGC
:停止系统程序。
- 然后采用单线程进行 标记、清理和压缩整理。
- 好空闲出来一批 Region 来供下一次
MixedGC
使用。- 这个过程是非常耗时的。
5.7 G1
垃圾收集器优化建议
- 假设参数(
-XX:MaxGCPauseMills
最大暂停时间)设置的值很大,导致系统运行很久。
年轻代 可能都占用了 堆内存 的60%
了,此时才触发 年轻代GC。- 那么存活下来的对象可能就会很多。
此时就会导致 Survivor区域 放不下那么多的对象,就会进入 老年代 中。- 或者是 年轻代GC 过后,存活下来的对象过多。
导致进入 Survivor区域 后触发了 动态年龄判定 规则。
达到了 Survivor区域 的 50%,也会快速导致一些对象进入老年代中。- 所以这里核心还是在于调节(
-XX:MaxGCPauseMills
最大暂停时间)这个参数的值。
- 在保证 年轻代GC 别太频繁的同时,还得考虑每次 GC 过后的存活对象有多少。
- 避免 存活对象 太多快速进入 老年代,频繁触发
MixedGC
。
5.8 每秒几十万并发,优化 JVM
Kafka
类似的支撑高并发消息系统。
- 对于 Kafka 来说,每秒处理 几万 甚至 几十万 消息时很正常的。
一般来说部署 Kafka 需要用大内存机器。- 比如:64G,也就是说可以给 年轻代 分配个
30-40G
的内存,用来支撑高并发处理。
- 这里就涉及到一个问题了,以前常说的对于 Eden 区的 YoungGC 是很快的。
这种情况下它的执行还会很快吗?
- 很显然,不可能,因为内存太大,处理还是要花不少时间的。
假设30-40G
内存回收可能最快也要几秒钟。- 按 Kafka 这个并发量,放满
30-40G
的 Eden 区可能也就一两分钟吧。- 那么意味着整个系统每运行一两分钟。
就会因为 YoungGC 卡顿几秒钟没法处理新消息,显然是不行的。
- 那么对于这种情况如何优化了。
- 我们可以使用
G1
收集器,设置(-XX:MaxGCPauseMills
为 50ms)。- 假设 50ms 能够回收
30-40G
内存。
然后 50ms 的卡顿其实完全能够接受,用户几乎无感知。
那么整个系统,就可以在卡顿几乎无感知的情况下,一边处理业务一边收集垃圾。
G1
天生就适合这种 大内存 机器的 JVM 运行。
可以比较完美的解决 大内存 垃圾回收时间过长 的问题。
三、如何选择垃圾收集器
- 优先 调整堆的大小,让服务器自己来选择。
- 如果内存小于 100M,使用 串行收集器。
- 如果是 单核,并且没有 停顿时间 的要求,串行 或 JVM 自己选择。
- 如果允许 停顿时间 超过 1秒,选择 并行 或 JVM 自己选。
- 如果 响应时间 最重要,并且不能超过 1秒,使用 并发收集器。
- 下图有连线的可以搭配使用,官方推荐使用
G1
,因为性能高。