一、垃圾收集算法
1. 标记-清除算法
- 首先标记出所有需要回收的对象。
- 在标记完成后,统一回收所有被标记的对象。
- 它是最基础的收集算法,效率也很高。
- 但是会带来两个明显的问题:效率问题、空间问题(标记清除后会产生大量不连续的碎片)。
2. 复制算法
- 它可以将内存分为大小相同的两块,每次使用其中的一块。
- 当这一块的内存使用完后,就将还存活的对象复制到另一块去。
- 然后再把使用的空间一次清理掉。
- 这样就使每次的内存回收,都是对内存区间的一半进行回收。
3. 标记-整理算法
- 根据老年代的特点特出的一种标记算法,标记过程仍然与 “标记-清除” 算法一样。
- 但后续步骤,不是直接对可回收对象回收。
- 而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
4. 分代收集算法
- 这种算法没有什么新的思想,只是根据 对象存活周期 的不同将内存分为几块。
- 一般将 Java 堆分为 新生代 和 老年代。
- 这样我们就可以根据各个年代的特点,选择合适的垃圾收集算法。
- 比如:在新生代中,每次收集都会有大量对象(近 99%)死去。
所以可以选择 复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。 - 而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保。
所以我们必须选择 “标记-清除” 或 “标记-整理” 算法进行垃圾收集。 - 注意,“标记-清除” 或 “标记-整理” 算法会比 复制算法 慢
10 倍以上。
二、垃圾收集器
- 如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
- 虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。
- 因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器。
- 我们能做的就是根据具体应用场景,选择适合自己的垃圾收集器。
- 如果有一种四海之内、任何场景下都适用的完美收集器存在。
那么我们的 Java 虚拟机,就不会实现那么多不同的垃圾收集器了。
1. Serial 收集器
Serial (串行)收集器是最基本、历史最悠久的垃圾收集器了。- 这个收集器是一个单线程收集器了。
- 它的 “单线程” 的意义,不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作。
- 更重要的是它在进行垃圾收集工作的时候,必须暂停其他所有的工作线程(
Stop The World ),直到它收集结束。
- 虚拟机的设计者们当然知道(
Stop The World )带来的不良用户体验。 所以在后续的垃圾收集器设计中,停顿时间在不断缩短(仍然还有停顿,寻找最优秀的垃圾收集器的过程仍然在继续)。
-XX:+UseSerialGC
-XX:+UseSerialOldGC
- 新生代采用 复制算法,老年代采用 标记-整理算法。
Serial 收集器优于其他垃圾收集器的地方:简单而高效(与其他收集器的单线程相比)。 Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。Serial Old 收集器是 Serial 收集器的老年代版本,它同样是一个单线程收集器。
- 一种用途是在 JDK-1.5 以及以前的版本中,与
Parallel Scavenge 收集器搭配使用。 - 另一种用途是作为
CMS 收集器的后备方案。
2. ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的 多线程 版本。
- 除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和
Serial 收集器完全一样。 - 默认的收集 线程数 跟 CPU 核数相同,当然也可以用参数(
-XX:ParallelGCThreads )指定收集线程数,但是一般不推荐修改。
-XX:+UseParNewGC
- 新生代采用 复制算法,老年代采用 标记-整理算法。
- 它是许多运行在
Server 模式下的虚拟机的首要选择。 - 除了
Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器)配合工作。
3. Parallel Scavenge 收集器
Parallel Scavenge 收集器类似于 ParNew 收集器。Parallel Scavenge 是 Server 模式(内存大于 2G ,2个CPU )下的默认收集器。
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 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 虚拟机第一款真正意义上的 并发收集器。 - 它第一次实现了让垃圾收集线程 与 用户线程(基本上)同时工作。
-XX:+UseConcMarkSweepGC
- 从名字中的
Mark Sweep 这两个词可以看出,CMS 收集器是一种 “标记-清除” 算法实现的。 它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。 - 整个过程分为四个步骤。
- 初始标记: 暂停所有的其他线程,并记录下
GCRoots 直接能引用的对象,速度很快。 - 并发标记: 同时开启
GC 和 用户线程,用一个闭包结构去记录可达对象。
- 但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。
- 因为用户线程可能会不断的更新引用域,所以
GC 线程无法保证可达性分析的实时性。 - 所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间,因为用户程序继续运行,而导致标记产生变动的那一部分对象的标记记录。
这个阶段的停顿时间,一般会比 初始标记 阶段的时间稍长,远远比 并发标记 阶段时间短。 - 并发清理: 开启 用户线程,同时
GC 线程 开始对未标记的区域做清扫。
4.1 CMS 优缺点
- 并发收集。
- 低停顿。
- 对 CPU 资源敏感(会和服务抢资源)。
- 无法处理 浮动垃圾(在并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次
GC 再清理了)。 - 它使用的回收算法 “标记-清除” 算法,会导致收集结束时会有大量空间碎片产生。
当然通过参数(-XX:+UseCMSCompactAtFullCollection )可以让 JVM 在执行完标记清除后再做整理。 - 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况。
- 特别是在 并发标记 和 并发清理 阶段会出现,一边回收,系统一边运行。
- 也许没回收完就再次触发
FullGC ,也就是(“Concurrent Mode Failure”)。 - 此时会进入(Stop The World),用
Serial old 垃圾收集器来回收。
4.2 CMS 相关参数
-XX:+UseConcMarkSweepGC
-XX:ConcGCThreads
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction
-XX:CMSInitiatingOccupancyFraction
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+CMSScavengeBeforeRemark
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 采用 复制算法 回收,几乎不会有太多内存碎片。
- 每次根据允许的收集时间,优先选择回收价值最大的 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 相关参数
-XX:+UseG1GC
-XX:ParallelGCThreads
-XX:G1HeapRegionSize
-XX:MaxGCPauseMillis
-XX:G1NewSizePercent
-XX:G1MaxNewSizePercent
-XX:TargetSurvivorRatio
-XX:MaxTenuringThreshold
-XX:InitiatingHeapOccupancyPercent
-XX:G1HeapWastePercent
-XX:G1MixedGCLiveThresholdPercent
-XX:G1MixedGCCountTarget
5.6 G1 垃圾收集分类
- 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 需要用大内存机器。
- 比如:64G,也就是说可以给 年轻代 分配个
30-40G 的内存,用来支撑高并发处理。
- 这里就涉及到一个问题了,以前常说的对于 Eden 区的 YoungGC 是很快的,这种情况下它的执行还会很快吗?
- 很显然,不可能,因为内存太大,处理还是要花不少时间的,假设
30-40G 内存回收可能最快也要几秒钟。 - 按 Kafka 这个并发量,放满
30-40G 的 Eden 区可能也就一两分钟吧。 - 那么意味着整个系统每运行一两分钟,就会因为 YoungGC 卡顿几秒钟没法处理新消息,显然是不行的。
- 我们可以使用
G1 收集器,设置(-XX:MaxGCPauseMills 为 50ms)。 - 假设 50ms 能够回收
30-40G 内存,然后 50ms 的卡顿其实完全能够接受。 用户几乎无感知,那么整个系统就可以在卡顿几乎无感知的情况下,一边处理业务一边收集垃圾。
G1 天生就适合这种 大内存 机器的 JVM 运行,可以比较完美的解决 大内存 垃圾回收时间过长的问题。
三、如何选择垃圾收集器
- 优先调整堆的大小,让服务器自己来选择。
- 如果内存小于 100 M,使用 串行收集器。
- 如果是单核,并且没有 停顿时间 的要求,串行 或 JVM 自己选择。
- 如果允许 停顿时间 超过 1 秒,选择 并行 或 JVM 自己选。
- 如果 响应时间 最重要,并且不能超过 1 秒,使用 并发收集器。
- 下图有连线的可以搭配使用,官方推荐使用
G1 ,因为性能高。
|