Skip to main content

JVM垃圾收集器

1 垃圾收集算法

1.1 标记 --- 清除

标记清除算法分为两步

  1. 标记需要回收的对象 -- 标记
  2. 标记完成后统一回收被标记需要回收的对象。 -- 清除

优点:

  • 解决循环引用问题
  • 必要时才回收(当内存不足的情况)

缺点:

  • 标记和清除两个过程效率都不高
  • 会产生大量的不连续的内存碎片,空间碎片太多会导致以后程序中需要分配比较大的对象的时候,无法找到足够的连续的内存而不得不提前出发一次垃圾收集的动作。
  • 回收的时候会触发 stop the world

1.2 标记 --- 整理

和标记清除算法一样 标记整理也分为两步且第一步是相同的:

  1. 将存活的对象标记--标记
  2. 移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收---整理阶段

优点:

  • 不会产生内存碎片

缺点:

  • 整理的效率不高

1.3 复制算法

复制算法将内存划分为两个区间,在任意时间点,所有动态分配的对象都只能分配在其中一个区间(称为活动区间),而另外一个区间(称为空闲区间)则是空闲的。 当有效内存空间耗尽时,JVM将暂停程序运行,开启复制算法GC线程。接下来GC线程会将活动区间内的存活对象,全部复制到空闲区间,且严格按照内存地址依次排列,与此同时,GC线程将更新存活对象的内存引用地址指向新的内存地址

​ 此时,空闲区间已经与活动区间交换,而垃圾对象现在已经全部留在了原来的活动区间,也就是现在的空闲区间。事实上,在活动区间转换为空间区间的同时,垃圾对象已经被一次性全部回收。

1.4 GC的可达性分析算法

基本思路: 通过一系列成为 GC Roots 对象作为起点,从这些起点开始向下搜索,搜索过程的路径称为 Reference Chain(引用链) ,当一个对象没有任何引用链相连的时,证明此对象是不可用的,所以即便有些对象互相引用但是和 GC Roots 之间不可达,依然会 GC ,这种方式很好的解决了计数器法不能解决的循环引用问题。

图解

从上图根据可达性分析可以知道**:A、 B、 C、 D四个对象不会被GC,而 E、F虽然互相引用但是和GC Roots之间不可达**

可以作为 GC Roots 的对象包括:

  1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
  2. 方法区中类静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈中JNI引用的对象

2. 相关概念说明

  • 并行(Parallel):指多条 垃圾收集线程并行 工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上。
  • Minor GC 和 Major GC (Full GC)
    • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
    • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
  • 吞吐量: 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。 虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

3. 垃圾收集器

下面看一下垃圾收集器的相关图(来源Oracle官网博客已放入我的github文档目录):

图片

注:图中的 表示什么呢,以后的GC收集器的另一外的实现。比如 JDK11 G1 垃圾收集器。

上图展示了六种(或者说7种 包括G1)不同的收集器在新生代和老年代的垃圾收集。如果两个收集器之间有连线说明说明可以搭配使用 。·

JVM GC的组合

参数年轻代GC年老代和持久代
-XX:+UseSerialGCSerialSerial Old
-XX:+UseParallelGC并行回收GC(Parallel Scavenge)Parallel Old
-XX:+UseConcMarkSweepGCParNew GC并发GC 当出现concurrent Mode failure时采用Serial Old
-XX:+UseParNewGCParNew GCSerial Old
-XX:+UseParalledlOldGC并行回收GC(Parallel Scavenge)Parallel Old
-XX:+UseParalledlOldGC -XX:+UseParNewGCPar New GCParallel Old 当出现concurrentMode failure 或promotion failed时采用Serial Old

4. Serial收集器

  • 特性: 这个收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。停止其他的线程工作就是我们常说的 Stop the world

  • 应用场景:

    Serial 收集器是虚拟机运行在 Client 模式下的默认新生代收集器。

    可以用命令

    java -version 查看

    一般情况下都是Server模式(Windows 平台可以搜一下jvm.cfg文件)

  • 优点:

    简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。对于自由一个CPU来说,由于现在的电脑大多数都是多核心CPU所以这个收集器就很少用

  • 原理图解:

    图解

  • 适用范围:

    • 新生代--Serial
    • 老年代--Serial Old

    所以这个收集器包含了处理新生代的 Serial(复制算法) 收集器和处理老年代的 Serial Old(标记整理算法) 收集器

5. ParNew收集器

  • 特性:

    ParNew 收集器其实就是 Serial(新生代) 收集器的多线程版本,除了使用了多线程以外其他的基本上都和 Serial(新生代) 收集器差不多。

  • 应用场景:

    ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。

    很重要的原因是:除了 Serial 收集器外,目前只有它能与 CMS 收集器配合工作。 在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器—— CMS 收集器,这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器 Parallel Scavenge 配合工作,所以在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择 **ParNew**或者 Serial 收集器中的一个。

  • 优点:

    多线程收集器

  • 原理图解:

    图解

  • 适用范围:

    • 新生代

6. Parallel Scavenge收集器

  • 特性:

    Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

  • 应用场景:

    停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

  • 优点:

    GC自适应调节,Parallel Scavenge收集器有一个参数-XX:+UseAdaptiveSizePolicy。当这个参数打开之后,就不需要手工指定新生代的大小、Eden与Survivor区的比例、晋升老年代对象年龄等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略(GC Ergonomics)。

  • 试用范围:

    • 新生代GC

7. Parallel Old收集器

Parallel ScavengeParallel old 属于同一个类型的收集器但是处理里的区域不同。前一个是新生代后一个是老年代。

  • 特性:

    Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和**“标记-整理”**算法。

  • 应用场景:

    在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器,老年代除了Serial Old收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的“拖累”,使用了Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果,由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合。

  • 图解:

    图解

  • 适用范围:

    • 老年代

8. CMS收集器

  • 特性:

    CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于**“标记-清除”**算法实现的。

    CMS 工作的的个步骤:

    • 初始化标记(CMS initial mark): 仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。 —— 串行
    • 并发标记(CMS Concurrent mark): 进行GC Roots Tracing的过程,在整个过程中耗时最长。— 并发
    • 重新标记(CMS remark): 为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。 — 并行 多个CG线程同时工作
    • 并发清除(CMS concurrent sweep):

    由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间

  • 图解:

    图解

  • 适用范围:

    • 老年代
  • 优点:

    并发收集低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)

  • 缺点:

    • 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。

    • 无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。

    • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。-- 产生垃圾是标记清除算法的通病。

      但是往往老年代还有很大剩余空间,但无法找到足够大的连续空间来分配当前对象,不得不触发一次 Full GC CMS的解决方案是使用UseCMSCompactAtFullCollection参数(默认开启),在顶不住要进行Full GC时开启内存碎片整理。在碎片整理过程需要 STW,停顿时间比正常的 Full GC STW 时间更长

9. G1收集器

  • 特点:

    • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
    • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
    • 空间整合 G1从整体来看是基于**“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”**算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
    • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

    横跨整个堆内存

    G1将整个个Java堆划分成为多个大小相等的独立区域(Region),虽然新生代和老年代的概念还保留。但新生代和老年代不再是物理隔离的概念了,而都是一部分Region(不需要连续)的集合。

    避免全表扫描

    G1把Java堆分为多个Region,就是“化整为零”。但是Region不可能是孤立的,一个对象分配在某个Region中,可以与整个Java堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个Java堆才能保证准确性,这显然是对GC效率的极大伤害。

    为了避免全堆扫描的发生,虚拟机为G1中每个Region维护了一个与之对应的Remembered Set。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

  • G1收集步骤

    • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
    • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行
    • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行
    • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率.

    图解

    美团对于G1 GC

10. 什么时候会发生GC

这里说的 GC 包括了**Minor GC** 和 Full GC 两种。什么时候发生GC这里也分为两种:

  • 程序调用 System.gc() 通过手动的方式在程序中调用方法
  • JVM 自身决定 GC 触发的时机
    • Minor GC
      • JVM无法为一个新的对象在新生代分配空间触发 Minor GC
    • Full GC
      • 老年代空间不足或者永久代空间不足(JDK8 元数据区)

11. 总结

收集器串行、并行or并发新生代/老年代算法目标适用场景
Serial串行新生代复制算法响应速度优先单CPU环境下的Client模式
Serial Old串行老年代标记-整理响应速度优先单CPU环境下的Client模式、CMS的后备预案
ParNew并行新生代复制算法响应速度优先多CPU环境时在Server模式下与CMS配合
Parallel Scavenge并行新生代复制算法吞吐量优先在后台运算而不需要太多交互的任务
Parallel Old并行老年代标记-整理吞吐量优先在后台运算而不需要太多交互的任务
CMS并发老年代标记-清除响应速度优先集中在互联网站或B/S系统服务端上的Java应用
G1并发both标记-整理+复制算法响应速度优先面向服务端应用,将来替换CMS

1 Minor GC 只包含新生代 2 Full GC 包含了老年代和新生代(也就是说Full GC过程伴随着有Minor GC)

参考文档:

https://www.jianshu.com/p/50d5c88b272d

https://crowhawk.github.io/2017/08/15/jvm_3/

https://tech.meituan.com/2017/12/29/jvm-optimize.html

https://blogs.oracle.com/jonthecollector/our-collectors

http://ifeve.com/useful-jvm-flags-part-7-cms-collector/