图解 JVM 垃圾回收(二)
- 1.垃圾收集器
- 1.1 内存分配与回收策略
- 1.2 Serial 收集器
- 1.3 Parallel Scavenge 收集器
- 1.4 ParNew 收集器
- 1.5 CMS 收集器
- 1.6 G1 收集器
- 2.Full GC 的触发条件
1.垃圾收集器
Java 虚拟机提供了多种垃圾回收器,每种回收器有其特定的用途和优势。以下是常见的垃圾回收器:
连线表示垃圾收集器可以配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程。
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
1.1 内存分配与回收策略
JVM 在进行 GC 时,并非每次都对堆内存(新生代、老年代;方法区)区域一起回收的,大部分时候回收的都是指新生代。
针对 HotSpot VM 的实现,它里面的 GC 按照回收区域又分为两大类:部分收集(Partial GC
),整堆收集(Full GC
)。
- 部分收集:不是完整收集整个 Java 堆的垃圾收集。其中又分为:
- 新生代收集(
Minor GC
/Young GC
):只是新生代的垃圾收集。 - 老年代收集(
Major GC
/Old GC
):只是老年代的垃圾收集。目前,只有 CMS GC 会有单独收集老年代的行为。很多时候 Major GC 会和 Full GC 混合使用,需要具体分辨是老年代回收还是整堆回收。 - 混合收集(
Mixed GC
):收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 GC 会有这种行为。
- 新生代收集(
- 整堆收集:收集整个 Java 堆和方法区的垃圾。
1.2 Serial 收集器
-XX:+UseSerialGC
-XX:+UseSerialOldGC
Serial 是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
- Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。
- Serial Old 收集器是 Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK
1.5
以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。 - 新生代采用 复制 算法,老年代采用 标记-整理 算法。
1.3 Parallel Scavenge 收集器
-XX:+UseParallelGC
(新生代)-XX:+UseParallelOldGC
(老年代)
Parallel 收集器其实就是 Serial 收集器的 多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器类似。默认的收集线程数 跟 CPU 核数 相同,当然也可以用参数(-XX:ParallelGCThreads
)指定收集线程数,但是一般不推荐修改。
Parallel Scavenge 收集器关注点是 吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的 停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解的话,可以选择把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用 复制 算法,老年代采用 标记-整理 算法。
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本。使用 多线程 和 标记-整理 算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器(JDK 8 默认的新生代和老年代收集器)。
1.4 ParNew 收集器
-XX:+UseParNewGC
(新生代)
ParNew 收集器其实跟 Parallel 收集器很类似,区别主要在于它可以和 CMS 收集器配合使用。
新生代采用 复制 算法。
1.5 CMS 收集器
-XX:+UseConcMarkSweepGC
(老年代)
CMS(Concurrent Mark Sweep
),Mark Sweep
指的是 标记-清除 算法。
CMS 收集器是一种以获取 最短回收停顿时间 为目标的收集器。它非常符合在注重用户体验的应用上使用,它是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程(STW),并记录下 GC Roots 直接能引用的对象,速度很快。
- 并发标记: 并发标记阶段就是从 GC Roots 的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
- 并发清理: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
- 并发重置:重置本次 GC 过程中的标记数据。
CMS 主要优点:并发收集、低停顿。但是它有下面几个明显的缺点:
- ⭕ 对 CPU 资源敏感(会和服务抢资源)。
- ⭕ 无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾只能等到下一次 GC 再清理了)。
- ⭕ 它使用的 “标记-清除” 回收算法会导致收集结束时会有大量空间碎片产生,通过参数
-XX:+UseCMSCompactAtFullCollection
可以让 JVM 在执行完标记清除后再做整理,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 - ⭕ 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发 Full GC,也就是 “Concurrent Mode Failure”,此时会进入 “Stop The World”,用 Serial Old 垃圾收集器来回收。
1.6 G1 收集器
G1(Garbage-First
)是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。G1 把堆划分成多个大小相等的 独立区域(Region
),新生代和老年代不再物理隔离。
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set
,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set
,在做可达性分析的时候就可以避免全堆扫描。
G1 收集器的运作大致可划分为以下几个步骤:
- 初始标记(
Initial Marking
):仅仅只是标记一下 GC Roots 能直接关联到的对象。 - 并发标记(
Concurrent Marking
):从 GC Roots 开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。 - 最终标记(
Final Marking
):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB(Snapshot At The Beginning,原始快照)记录。 - 筛选回收(
Live Data Counting and Evacuation
):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
它具备以下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 Java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的 “标记-清除” 算法不同,G1 从整体来看是基于 “标记-整理” 算法实现的收集器;从局部上来看是基于 “标记-复制” 算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
从 JDK 9 开始,G1 垃圾收集器成为了默认的垃圾收集器。
2.Full GC 的触发条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
- 调用
System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。 - 老年代空间不足。老年代空间不足的常见场景为前文所讲的:大对象直接进入老年代、长期存活的对象进入老年代等。为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过
-Xmn
虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过-XX:MaxTenuringThreshold
调大对象进入老年代的年龄,让对象在新生代多存活一段时间。 - 空间分配担保失败。使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。
- JDK
1.7
及以前的永久代空间不足(1.7
之后元空间不足)。在 JDK1.7
及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出java.lang.OutOfMemoryError
。为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 - Concurrent Mode Failure。执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。