JVM第三讲:深入理解java虚拟机之垃圾回收算法?CMS垃圾回收的基本流程?对象引用类型? - Go语言中文社区

JVM第三讲:深入理解java虚拟机之垃圾回收算法?CMS垃圾回收的基本流程?对象引用类型?


JVM第三讲:深入理解java虚拟机之垃圾回收算法?CMS垃圾回收的基本流程?对象引用类型?

摘要:本文讲解讲解常见的垃圾收集算法,首先思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收? 然后讲解内存回收的具体实现-垃圾收集器 ,最后对Java中 对象引用类型及具体使用场景 做了探讨,jvm的自动垃圾回收策略使得程序员摆脱了编程中繁杂的内存管理,可以把精力专注于系统业务。

思考三个问题:1、哪些内存需要回收?2、什么时候回收?3、如何回收?

讲讲你对垃圾回收机制的理解(问题问的很宽泛,就看你怎么回答和理解)
什么是垃圾,为什么要回收,不回收有什么问题,jvm有哪些区域,分别采用哪些回收方案,每个方案有哪些优缺点,为什么适合这个区域 讲讲你对垃圾回收机制的理解(问题问的很宽泛,就看你怎么回答和理解)
面试官: 为什么年轻代e,s1,s2是8:1:1
我:xxxx,内存利用率能方面讲
面试官: 工作中有解决过gc问题吗?什么场景下出现的,你如何去排查和解决

1、哪些内存需要回收?(共享区)

  • 程序计数器、虚拟机栈、本地方法栈是每个线程私有的内存空间,随线程而生,随线程而亡,无需考虑回收问题
  • JVM中的方法区需要进行内存回收

2、什么时候回收?

1)判断对象是否死亡

  • 1、引用计数算法 (废弃)
    对象被引用就+1,难以解决循环引用问题

  • 2、可达性算法(栈、方法区的引用对象)
    1)概念:通过一系列称为 “GC roots” 的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链当一个对象到GC roots没有任何引用链相连时,证明此对象不可用

    2)GC roots的对象包括
    GC roots是啥:堆外指向堆内的引用

    1、虚拟机栈中引用的对象(堆中)
    2、本地方法栈 native引用的对象
    3、方法区中 类静态属性 引用的对象
    4、方法区中常量引用的对象

    • 存在的问题:在多线程环境下,其他线程可能会更新已经访问过的对象中的引用,从而造成误报(将引用设置为 null)或者漏报(将引用设置为未被访问过的对象)

    3)引用的分类

    • 强引用(new),软引用(soft),弱引用(weak),虚引用(Phantom)
    • 它们的差异见第六小章

    4)当一个对象不可达GC Roots时,这个对象并不会立马被回收,被真正的回收需要经历两次标记

    • 如果没有GC roots相连接的引用链,他将第一次标记并进行筛选,看是否有必要执行finalize方法;
    • 如果有必要执行finalize方法,对象将被放置在F-Queue队列中,虚拟机会触发一个Finalize()线程去执行,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize中拯救自己,只要重新引用链上的任何一个对象建立关联。

2)Java垃圾回收时间

  1. 自动:不需要显示释放对象内存,虚拟机自行执行;
  2. GC时间:在虚拟机空闲、堆内存不足时触发,低优先级垃圾回收线程;
  3. GC对象:没任何引用的对象(可达性算法)

3) Stop-the-world 以及安全点

  • 在 Java 虚拟机里,传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause);
  • Java 虚拟机中的 Stop-the-world 是通过安全点(safepoint)机制来实现的。当 Java 虚
    拟机收到 Stop-the-world 请求,它便会等待所有的线程都到达安全点,才允许请求 Stop-the-world 的线程进行独占的工作;
  • 安全点的初始目的并不是让其他线程停下,而是找到一个稳定的执行状态。在这个执行状态下,Java 虚拟机的堆栈不会发生变化。这么一来,垃圾回收器便能够“安全”地执行可达性分析。
  • 在有安全点请求的情况下,Java 虚拟机会将安全点检测访问的内存所在的页设置为不可读,并且定义一个 segfault 处理器,来截获因访问该不可读内存而触发 segfault 的线程,并将它们挂起
  • Java运行的状态
    • 状态1:执行 JNI 本地代码
      • 安全点
    • 状态2:解释执行字节码
      • 字节码与字节码之间皆可作为安全点。Java 虚拟机采取的做法是,当有安全点请求时,执行一条字节码便进行一次安全点检测
    • 状态3:执行即时编译器生成的机器码
      • 由于这些代码直接运行在底层硬件之上,不受 Java 虚拟机掌控,因此在生成机器码时,即时编译器需要插入安全点检测,以避免机器码长时间没有安全点检测的情况。HotSpot 虚拟机的做法便是在生成代码的方法出口以及非计数循环的循环回边(back-edge)处插入安全点检测

3、垃圾回收器的原理是什么?有什么办法手动进行垃圾回收?

  • 对于GC来说,当程序员创建对象时,GC就开始监控这个对象的地址、大小以及使用情况。
  • 通常,GC采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式确定哪些对象是”可达的”,哪些对象是”不可达的”。当GC确定一些对象为”不可达”时,GC就有责任回收这些内存空间。
  • 可以。程序员可以手动执行System.gc(),通知GC运行,但是Java语言规范并不保证GC一定会执行。

4、如何回收死亡的对象?垃圾回收算法 方法论

1)标记-清除算法 直接回收不存活的(老年代)

  • 分为标记和清除两个过程,首先标记处所有需要回收的对象,在标记完成后统一回收所有被标记的对象,清理过程如下图所示在这里插入图片描述
  • 缺点分配效率较低
    • Java 虚拟机则需要逐个访问列表中的项,来查找能够放入新建对象的空闲内存
  • 空间问题:标记清除后会产生大量不连续的内存碎片

2)复制算法 (新生代)

  • 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。如下图所示:
    在这里插入图片描述
  • 缺点:堆内存使用率低,只有原来的一半。
  • 把Eden:From Survivor:To Survivor空间大小设成8:1:1,对象总是在Eden区出生,若Eden区满,触发minor GC,若GC后,存活的对象太多,to survivor内存不够时,通过分配担保机制复制到老年代

3)标记 - 整理算法(老年代)

  • 和标记-清除算法类似,在清除对象的时候先将可回收对象移动到一端,然后清除掉端边界以外的对象
    在这里插入图片描述

    优点:1、解决大量内存碎片问题;2、当对象存活率较高时,效率也很好
    缺点:压缩算法的性能开销大

4)分代收集算法 根据对象存活周期的不同,将内存空间划分为几块

  • 1、新生代(复制算法)
  • 2、老年代(标记-清除算法,标记-整理算法)
  • 当前商业虚拟机都采用这种方式

讲一下新生代、老年代、永久代的区别
在这里插入图片描述
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。而新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收

  • 新生代中一般保存新出现的对象,所以每次垃圾收集时都发现大批对象死去,只有少量对象存活,便采用了复制算法,只需要付出少量存活对象的复制成本就可以完成收集;

  • 老年代中一般保存存活了很久的对象,他们存活率高、没有额外空间对它进行分配担保,就必须采用“标记-清理”或者“标记-整理”算法

  • 永久代就是JVM的方法区。在这里都是放着一些被虚拟机加载的类信息,静态变量,常量等数据。这个区中的东西比老年代和新生代更不容易回收

  • 默认情况下,Java 虚拟机采取的是一种动态分配的策略(对应 Java 虚拟机参数 -
    XX:+UsePSAdaptiveSurvivorSizePolicy
    ),根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例

    • 你也可以通过参数 -XX:SurvivorRatio 来固定这个比例

Action1: 为什么新生代要分Eden和两个 Survivor 区域?

  • 如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代。老年代很快被填满,触发Major GC。老年代的内存空间远大于新生代,进行一次Full GC消耗的时间比Minor GC长得多,所以需要分为Eden和Survivor;
  • Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历15次Minor GC还能在新生代中存活的对象,才会被送到老年代
  • 设置两个Survivor区最大的好处就是解决了碎片化,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)

Action2:Java堆老年代( Old ) 和新生代 ( Young ) 的默认比例?

  • 默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。

  • 其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,Eden 和俩个Survivor 区域比例是 = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),

  • 但是JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。

Action3:JVM的 stop-the-world 机制非常不友好,有哪些解决之道?原理是什么?

  • 采用并行GC可以减少需要STW的时间。它们会在即时编译器生成的代码中加入写屏障或者读屏障

Action4:压测时出现频繁的gc容易理解,但是有时出现毛刺是因为什么呢?

  • Y轴应该是时间,那毛刺就是长暂停。一般Full GC就会造成长暂停

Action5:fullgc有卡顿,对性能很不利,怎么避免呢?

  • 通过调整新生代大小,使对象在其生命周期内都待在新生代中。这样一来,Minor GC时就可以收集完这些短命对象了

Action6:TLAB是什么?有什么作用?

  • TLAB(Thread Local Allocation Buffer,对应虚拟机参数 -XX:+UseTLAB,默认开启)。每个线程可以向 Java 虚拟机申请一段连续的内存,比如 2048 字节,作为线程私有的 TLAB
  • TLAB 是为了避免对象分配时对内存的竞争

内存分配与回收策略 对象的内存分配

  • 1、大多数情况下,对象在新生代eden区分配eden区没有足够的空间进行分配时,虚拟机将发起一次minor gc,如果本次 GC 后还是没有足够的空间,则将启用分配担保机制在老年代中分配内存;

  • 2、大对象(需要大量连续内存空间的java对象,超出eden区大小)直接进入老年代

  • 3、长期存活的对象将进入老年代:(存活轮次最多:15

    • JVM 采用分代收集的思想管理内存,给每个对象定义了一个年龄计数器。如果对象在eden出生后并经过第一次minor GC后仍然存活,将移动到survivor中,age++,对象在survivor区每经过一次minorGC,age++,age=15(对应虚拟机参数 -XX:+MaxTenuringThreshold)时,升到老年代。
    • HotSpot 会在对象头中的标记字段里记录年龄,分配到的空间只有4位,最多只能记录到15
  • 4、动态对象年龄判断(防止survivor满)

    • 如果在survivor空间中相同年龄所有对象大小的总和大于survivor空间的一半(对应虚拟机参数 -XX:TargetSurvivorRatio年龄大于或等于该年龄的对象就可以直接进入老年代,无需等到maxTenutingThreshold中要求的年龄。
  • 5、空间分配担保机制

    • 在y gc之前,jvm会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,大于表示安全;如果小于:检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小

5、垃圾收集器:

1)垃圾收集器就是内存回收的具体实现
2)HotSpot内存回收算法

  • 准确式GC 快速完成GC Roots引用链枚举

    引用链枚举时,应用OopMap数据结构,快速完成GC Roots引用链枚举,
    OopMap:在类加载完成时,存储 :寄存器和栈的 <偏移量,数据及数据类型>;<k,v>

  • 安全点检测程序有长时间执行特征 — 方法调用、循环跳转、异常跳转时

    1、仅需在安全点记录OopMap信息;若每条指令生成OopMap,则空间成本太大;
    2、程序执行时仅在安全点停下来GC

  • 多线程的主动式中断,使得各个线程都跑到安全点再停顿

    1、在安全点、创建的对象分配内存时,设置一个标志
    2、各个线程执行时主动轮询该标志,若为真,则中断挂起

  • 安全区域检测:代码中,引用关系不发生变化

    1、线程没有分配CPU时间,无法跑到安全点
    2、GC时可忽略该标识自己处于安全区域的线程
    3、要离开安全区域,需要收到系统已完成引用链枚举的信号

  • HotSpot虚拟机的垃圾回收器
    年轻代:serial收集器 parNew parallel scavenge G1
    老年代:parallel old serial oid CMS G1

5.1、垃圾收集器分类

为什么对垃圾收集器分类?

  • 大部分的 Java 对象只存活一小段时间,而存活下来的小部分 Java 对象则会存活很长一段时间

垃圾收集器可分为3类

  • 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。下图展示了7种作用于不同分代的收集器,
    • 其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,
      • 采用耗时较短的垃圾回收算法,让大部分的垃圾都能够在新生代被回收掉
    • 回收老年代的收集器包括Serial Old、Parallel Old、CMS,
    • 还有用于回收整个Java堆的G1收集器。

不同收集器之间的连线表示它们可以搭配使用
在这里插入图片描述
新生代的垃圾回收器共有三个:Serial,Parallel Scavenge 和 Parallel New

  • 1、serial收集器 (新生代默认收集器

    • 单线程在进行垃圾回收时,必须暂停其他所有的工作线程,直至他收集完成
    • 新生代,复制算法
  • 2、parNew收集器: 新生代

    • 是 serial 收集器的多线程版本,只有它和 serial 能配合 CMS 收集器工作,默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数
    • 新生代 复制算法
  • 3、ParNew Scanvenge收集器 新生代

    • 达到一个可控制的吞吐量。停顿时间和吞吐量不可能同时调优。
    • 吞吐量:指CPU用于运行用户代码的时间占总时间的比值

针对老年代的垃圾回收器也有三个

  • 1、Serial Old
    • 标记 - 整理算法 单线程
  • 2、Parallel Old
    • 标记 - 整理算法 多线程
  • 3、CMS收集器: 老年代
    • 一款以获取 最短回收停顿时间 为目标的收集器,是基于“标记-清除”算法实现的,重视服务的响应速速,希望系统停顿时间最短。
    • 产生背景: 解决STW问题
      • 将GC过程细化,区分出必须STW场景和非必须STW场景。从业务场景出发解决问题
    • CMS 在 Java 9中已被废弃
4个步骤特点
初始标记gc-roots能直接关联的对象 stop the world
并发标记进行gc tracing的过程
重新标记修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录 stop the world
并发清除基于标定结果,直接清除对象
缺点详情及解决方案
对cpu资源非常敏感
吞吐量低低停顿时间以牺牲吞吐量为代价的,导致CPU利用率不够高。
cms收集器无法处理浮动垃圾可能出现concurrent mode failure 而导致另一次full gc的产生解决方法:虚拟机启动后备预案,临时启用serial old收集器来重新进行老年代的垃圾回收
cms基于“标记-清除”,会产生大量内存碎片解决方案:开启内存碎片的合并整理过程
  • 5、G1收集器: /region/ 新生代 + 老年代
G1的特点详情
1、并发与并行G1能充分利用cpu、多核的硬件优势,使用多个cpu (CPU 或者CPU 核心) 来缩短stop-the-world 停顿的时间,部分其他收集器原本需要停顿 java 线程执行的GC动作,G1收集器仍然可以通过并发的方式让 java 程序继续执行
2、空间整合cms基于“标记-清除”,会产生大量内存碎片,G1是基于标记-整理的算法(两个region的数据是基于复制的),不会产生内存空间碎片
3、可预测的停顿g1能建立可预测的停顿时间模型,让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集的时间不超过N毫秒
4、分代收集虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆, 但是还是保留了分代的概念
实现思路:
  • 使用G1收集器时,将堆划分为相等的region,并且能和整个堆中任意的对象发生引用关系,优先回收价值大的region。每个Region都有一个Remembered Set,用来记录该Region对象的引用对象所在的Region。通过使用Remembered Set,在做可达性分析的时候就可以避免全堆扫描

  • 整体上看是“标记-整理”,局部看是“复制”,不会产生内存碎片

  • 总结:

需求使用的收集器
1.吞吐量优先Parallel Scavenge 新生代 复制算法 / parallel Old 老年代 标记-整理
2、重视服务响应速度,最短回收停顿时间Parallel Scavenge 新生代 复制算法 / CMS 老年代 并发的标记-清除
3、面向服务器端应用G1收集器
5.2、CMS收集器详解
5.2.1、CMS 垃圾收集的过程?
  • CMS 垃圾收集的过程网上通常有两个版本,4个步骤的和7个步骤的,两个版本其实都是对的。
    • 4个步骤应该主要是跟随周志明的说法,而 CMS 的相关论文其实也是按4个步骤介绍。
    • 7个步骤则应该更多是从 CMS 日志得出的说法,而7个步骤里其实也包含了上述的4个步骤,可以理解为7个步骤是更细的说法。
  1. 初始标记 initial mark

    • 遍历 GC Roots,标记gc-roots能直接关联的对象 stop the world
  2. 并发标记(Concurrent Mark)

    • 从初始标记阶段被标记为存活的对象作为起点,向下遍历,找出所有存活的对象。
    • 同时,由于该阶段是用户线程和GC线程并发执行,对象之间的引用关系在不断发生变化,对于这些对象,都是需要进行重新标记的,否则就会出现错误。为了提升重新标记的效率,JVM 会使用写屏障(write barrier)将发生引用关系变化的对象所在的区域对应的 card 标记为 dirty,后续只需要扫描这些 dirty card 区域即可,避免扫描整个老年代。
      • 卡表见6.4节
  3. 并发预处理(Concurrent Preclean)

    • 该阶段存在的意义主要是为了尽可能降低 Final Remark 阶段的耗时,因为 Final Remark 阶段是 STW 的。
    • 该阶段主要做的事是将上一阶段被标记为 dirty 的 card 所对应的区域进行重新扫描标记,处理并发阶段发生引用变化的对象。
  4. 可中断的并发预处理(Concurrent Abortable Preclean)

    • 该阶段和并发预处理做的事是基本一样的,也是主要处理 dirty card。区别在于并发预处理只执行一次,而本阶段会一直循环执行,直到触发终止条件。
    • 终止条件有以下几个:
      • 循环次数超过阈值 CMSMaxAbortablePrecleanLoops,默认是0,也就是没有循环次数的限制。
      • 处理时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是5秒。
      • Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认为50%。
    • 同时该阶段有一个触发前提:
      • Eden 区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold,默认是2M。
  5. 最终标记/重新标记(Final Remark)

    • STW(stop the world),主要做两件事:
      • 遍历 GCRoots,重新扫描标记
      • 遍历被标记为 dirty 的 card,重新扫描标记
  6. 并发清理(Concurrent Sweep)

    • 清理未使用的对象并回收它们占用的空间。
  7. 并发重置(Concurrent Reset)

    • 重置 CMS 算法用于打标的数据结构(markBitMap),为下一次收集做准备
5.2.2、CMS存在的问题
  1. 使用的标记-清除算法,可能存在大量空间碎片。

    • 调优:开启CMS压缩,查看参数是否合理。
    • // 开启CMS压缩,在FGC时执行压缩,默认为true
      -XX:+UseCMSCompactAtFullCollection 
      // 执行几次FGC才执行压缩,默认为0
      -XX:CMSFullGCsBeforeCompaction=0 
      
  2. 并发清理可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生

    • 调优:可能是触发GC的比例太高,适当调低该值。
    • // CMS触发GC的比例
      -XX:+UseCMSInitiatingOccupancyOnly
      -XX:+CMSInitiatingOccupancyFraction=80  (我们项目为80)
      
  3. 对CPU资源非常敏感。在并发阶段,会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4。

    • 调优:可能是并发线程数设置太高,适当调低该值。
    • // CMS并发线程数
      -XX:ConcGCThreads=X
      
  • 以上的调优只是针对一些可能性较大的问题给的建议,具体还是需要结合场景和完整的JVM参数去分析,各个参数可能都会影响到整体的GC效率。
5.2.3、Final Remark 阶段为什么还需要遍历 GCRoots?

这是因为 CMS 的写屏障(write barrier)并不是对所有会导致引用变化的字节码生效,例如不支持 astore_X(把栈顶的值存到本地变量表)。

至于为什么不为 astore_X 添加写屏障,R 大认为是栈和年轻代属于数据快速变化的区域,对于这些区域使用写屏障的收益比较差。

5.2.4、Final Remark 阶段还需要遍历 GC Roots,那之前的标记工作不是白做了?

不是的。

  • 在三色标记法中(见下面介绍),如果扫描到被标记为黑色的对象就会终止,而之前的并发标记和预处理已经完成了绝大部分对象的标记,也就是此时大部分对象已经是黑色了,因此 Final Remark 阶段的工作其实会减少很多。
  • 简单来说就是:遍历的广度不变,但是深度变浅了。
5.2.5、三色标记算法?
  • 三色标记算法由 Edsger W. Dijkstra 等人在1978年提出,是一种增量式垃圾回收算法,增量式的意思是慢慢发生变化的意思,也就是 GC 和 mutator(应用程序)一点点交替运行的手法。

    • 与其相反的则是停止型GC,也就是GC时,mutator 完全停止,GC结束再恢复运行。
  • 三色标记算法顾名思义就是将 GC 中的对象分为三种颜色,这三种颜色和所包含的意思如下:

    • 白色:还未搜索过的对象。在回收周期的开始阶段,所有对象都为白色,而在回收周期结束时,所有白色对象均为不可达对象,也就是要回收的对象。
    • 灰色:正在搜索的对象。已经被搜索过的对象,但是该对象引用的对象还未被全部搜索完毕。
    • 黑色:搜索完成的对象。本身及其引用的所有对象都被搜索过,黑色对象不会指向白色对象,同时黑色对象不会被重新搜索,除非颜色发生变化。
  • 我们以 GC 标记-清除算法为例简单的说明一下。

    • GC 开始运行前所有的对象都是白色。GC 一开始运行,所有从根能到达的对象都会被标记为灰色,然后被放到栈里。GC 只是发现了这样的对象,但还没有搜索完它们,所以这些对象就成了灰色对象。

    • 灰色对象会被依次从栈中取出,其子对象也会被涂成灰色。当其所有的子对象都被涂成灰色时,该对象就会被涂成黑色。当 GC 结束时已经不存在灰色对象了,活动对象全部为黑色,垃圾则为白色。

  • 下面是一个三色标记算法的示例动图,大家参考着理解。
    在这里插入图片描述

  • 明白了三色标记算法后,再回过头去看5.2.3,是不是顿时就明白了。

5.2.6、三色标记算法存在的问题?

三色标记算法是增量式垃圾回收算法,mutator可能会随时改变对象引用关系,因此在并发下会存在漏标和错标(多标)。

  1. 漏标
    • 直接通过一个简单的例子来看:

    • 假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1和2,也就是到了时刻3的场景,GC线程继续执行。

    • 此时对象Z只被黑色对象X所引用,而黑色对象是不会被继续扫描的,因此扫描结束后Z仍然是白色对象,也就是时刻4,此时白色对象Z则会被当做垃圾而回收。

在这里插入图片描述

  1. 错标(多标)
    • 直接通过一个简单的例子来看:

    • 假设当GC线程执行到时刻1时,此时应用线程先执行了步骤1,也就是到了时刻2的场景,GC线程继续执行。

    • 此时对象Z是灰色对象,GC线程对其进行搜索,搜索结束后将其标记为黑色,也就是时刻3,此时对象Z其实没有到GC Roots的引用,理应被回收,但是因为被错误的标记为黑色,而在本次GC中存活了下来。

在这里插入图片描述

  • 错标和漏标都是三色标记算法存在的问题,但是两者带来的后果有本质的不同。

    • 错标使得死亡的对象被当做存活,导致出现浮动垃圾,此时不影响程序的正确性,这些对象下次GC时回收就可以了。
    • 漏标使得存活的对象被当做死亡,这个会导致程序出错,带来不可预知的后果,这个是不能接受的,因此漏标是三色标记算法需要解决的问题
  • 通过实验追踪,Wilson 发现,只有当以下两个条件同时满足时才会出现漏标问题:

    • 1)将某一指向白色对象的引用写入黑色对象
    • 2)从灰色对象出发,最终到达该白色对象的所有路径都被破坏
  • 解决方案:5.2.7 所示的增量更新和起始快照

5.2.7、增量更新和起始快照

为了解决三色标记算法的漏标问题,产生了两种比较著名的解决方案:增量更新和起始快照,CMS 和 G1 就是采用了这两种解决方案,

  • CMS 使用的增量更新,
  • G1使用的起始快照。

漏标问题的出现必须同时满足上述的两个条件,因此解决办法只需破坏两个条件之一即可。

  1. 增量更新(Incremental update)

    • 使用写屏障(write barrier)拦截所有新插入的引用关系,将其记录下来,最后以这些引用关系的源头作为根,重新扫描一遍即可解决漏标问题。
    • 增量更新破坏的是条件1,当插入黑色对象到白色对象的引用时,写屏障会记录下该引用,后续重新扫描。
    • 以上面的漏标为例,就是拦截步骤1:X.b=Y.a,记录下X,然后重新扫描对象X。
  2. 起始快照(SATB,snapshot at the begin)

    • 使用写屏障拦截所有删除的引用关系,将其记录下来,然后将被删除的引用关系所指向的对象会被当作存活对象(非白色),重新扫描该对象。

    • SATB 抽象的说就是在一次GC开始时刻是存活的对象,则在本次GC中都会被当做存活对象,此时的对象形成一个逻辑“快照”,这也是起始快照名字的由来。

    • 起始快照破坏的是条件2,当到白色对象的引用断开时,写屏障会记录下该引用,将该对象当作存活对象,后续继续扫描该对象的引用。

    • 以上面的漏标为例,就是拦截步骤2:Y.a=null,将Z作为存活对象,然后重新扫描对象Z。

5.2.8、CMS中的 Final Remark(重新标记)阶段⽐较慢,怎么分析和解决?

CMS 的整个垃圾回收过程中只有2个阶段是 stop the world,一个是初始标记,一个是重新标记,初始标记只标记GC Roots直达的对象,因此一般不会耗时太久,而重新标记出现耗时久的现象则比较多见,通常如果CMS GC较慢,大多都是重新标记阶段较慢导致的

  • Final Remark 阶段比较慢,比较常见的原因是在并发处理阶段引用关系变化很频繁,导致 dirty card 很多、年轻代对象很多。

  • 比较常见的做法可以在 Final Remark 阶段前进行一次 YGC,这样年轻代的剩余待标记对象会下降很多,被视为GC Root 的对象数量骤减, Final Remark 的工作量就少了很多。

  • // 在remark之前尝试进行清理,默认值为false 
    -XX:+CMSScavengeBeforeRemark
    
  • 通常增加 -XX:+CMSScavengeBeforeRemark 都能解决问题,但是如果优化后还是耗时严重,则需要进一步看具体是哪个小阶段耗时严重。

  • Final Remark 具体包含了若干个小阶段:weak refs processing、class unloading、scrub string table等,从日志里可以看出来每个小阶段的耗时,根据耗时的阶段再进行针对性的分析,可以查阅源码或者查阅相关资料来帮助分析。

  • 以比较常见的 weak refs processing 为例:

    • 这边的 weak refs 不是单指 WeakReference,而是包括了:SoftReference、WeakReference、FinalReference、PhantomReference、JNI Weak Reference,这边应该是除了强引用外的所有引用都被归类为 weak 了。

    • 因此,我们首先添加以下配置,打印出GC期间引用相关更详细的日志。

    • // 打印GC的详细信息
      -XX:+PrintGCDetails 
      // 打印在GC期间处理引用对象的时间(仅在PrintGCDetails时启用)
      -XX:+PrintReferenceGC
      
    • 然后根据每个引用的耗时,定位出耗时严重的引用类型,然后查看项目中是否存在对该引用类型不合理的使用。

  • 另外一种比较简单粗暴的办法是可以通过增加引用的并行处理来尝试解决,通常会有不错的效果。

  • // 启用并行引用处理,默认值为false
    -XX:+ParallelRefProcEnabled
    
  • 而如果是 scrub string table 阶段耗时,则可以分析项目中是否存在不合理的使用 interned string,其他的也类似。

5.3、G1收集器详解

定义:G1(Garbage First),是一种服务端应用使用的垃圾收集器,目标是用在多核、大内存的机器上,它在大多数情况下可以实现指定的GC暂停时间,同时还能保持较高的吞吐量。

G1产生的背景: 大堆JVM问题

  • 将堆拆分成若干个region,并不是GC整个堆,而是GC部分堆。从数据结构出发解决问题
  • 横跨新生代和老年代的垃圾回收器

特点

  • 面向服务端应用的垃圾收集器,以获取最短回收停顿时间为目标的收集器 两次停顿
    - G1 的全称是 Garbage-First, 意为垃圾优先, 哪一块的垃圾最多就优先清理它
    - G1 GC 最主要的设计目标是: 将 STW 停顿的时间和分布, 变成可预期且可配置的。被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征
    - G1 收集器在后台维护了一个优先列表, 每次根据允许的收集时间, 优先选择回收价值最大的Region( 这也就是它的名字 Garbage-First 的由来)
    - JDK1.9默认垃圾收集器

传统垃圾回收器内存模型如下图所示
在这里插入图片描述

G1内存模型如下图所示

  • Humongous区域: 存放大对象的区域;
  • 相较于传统的内存模型,依旧保留eden、servivor、old区。但只是逻辑保留,并不是物理连续的。新创建的对象依旧是先存入Eden,再到servivor,最后进入old区。将整个 heap 分为若干个Region,eden、servivor、old区分别由若干个region组成。
  • Region:G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region 占有一块连续的虚拟内存地址。是GC的最小单位
  • RSet(Remembered Set):Region的一部分,用于存放其他 region 对象到本 region 的引用(points-in)。标记该 region 时,相当于该区的 GC roots 对象;
  • CSet(Collection Set):收集集合(CSet),代表每次 GC 暂停时回收的一系列目标分区;
    • 一般包含全部young 区的 region 和 部分old区 region(垃圾对象较多的region)

在这里插入图片描述

  • Card Table:一个 region 内部又被分为若干个 card
    • 定义:该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用,如果可能存在,那么我们就认为这张卡是脏的。
    • 作用:将region拆分的更细,扫描region的Rset时,可更加精确的定位到 region 的某一具体区域(card table),而不是整个region
      在这里插入图片描述

GC过程
GC前在这里插入图片描述
GC后
在这里插入图片描述
扫描过程
在这里插入图片描述

普通Young GC
GC步骤

  • 1、扫描gc roots对象
  • 2、更新Rset、扫描Rset
  • 3、扫描局部对象引用
  • 4、对象赋值转移
  • 5、处理引用
  • 6、释放空闲region

GC LOG

2020-12-02T15:24:13.238-0800: Total time for which application threads were stopped: 0.0043412 seconds, Stopping threads took: 0.0000095 seconds
{Heap before GC invocations=8 (full 0):
 garbage-first heap   total 102400K, used 74304K [0x00000007b9c00000, 0x00000007b9d00320, 0x00000007c0000000)
  region size 1024K, 60 young (61440K), 5 survivors (5120K)
 Metaspace       used 3166K, capacity 4568K, committed 4864K, reserved 1056768K
  class space    used 338K, capacity 392K, committed 512K, reserved 1048576K
2020-12-02T15:24:13.864-0800: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 4194304 bytes, new threshold 15 (max 15)
- age   1:    2048080 bytes,    2048080 total
- age   2:    2048080 bytes,    4096160 total
 4.865: [G1Ergonomics (CSet Construction) start choosing CSet, _pending_cards: 256, predicted base time: 1.83 ms, remaining time: 198.17 ms, target pause time: 200.00 ms]
 4.865: [G1Ergonomics (CSet Construction) add young regions to CSet, eden: 55 regions, survivors: 5 regions, predicted young region time: 46.69 ms]
 4.865: [G1Ergonomics (CSet Construction) finish choosing CSet, eden: 55 regions, survivors: 5 regions, old: 0 regions, predicted pause time: 48.52 ms, target pause time: 200.00 ms]
, 0.0045289 secs]
   [Parallel Time: 4.2 ms, GC Workers: 2]
      [GC Worker Start (ms): Min: 4865.2, Avg: 4865.2, Max: 4865.2, Diff: 0.0]
// 扫描gc Roots对象
      [Ext Root Scanning (ms): Min: 0.2, Avg: 0.2, Max: 0.2, Diff: 0.0, Sum: 0.3]
// 更新RSet,Rset是通过写屏障和缓冲区实现的,更新Rset,确保此时Rset是最新的。更新完后将缓冲区处理掉
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.1]
      [Processed Buffers: Min: 1, Avg: 1.0, Max: 1, Diff: 0, Sum: 2]
// 扫描Rset
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 扫描局部对象引用
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
// 赋值对象
      [Object Copy (ms): Min: 3.4, Avg: 3.6, Max: 3.8, Diff: 0.3, Sum: 7.2]
// 线程窃取算法,每个线程完成任务后会尝试帮其他线程完成剩余的任务
      [Termination (ms): Min: 0.1, Avg: 0.3, Max: 0.5, Diff: 0.4, Sum: 0.6]
         [Termination Attempts: Min: 77
                            
                            版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/qq_28959087/article/details/86665793
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2023-01-02 09:49:21
  • 阅读 ( 162 )
  • 分类:算法

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢