这里所提到的到都是GC涉及到的一些概念,具体不同的收集器由于内存结构不同,并发串行不同,实现上不止下面这些东西

STW GC中Stop the world

即在执行垃圾收集算法时,Java应用程序的其他所有除了垃圾收集收集器线程之外的线程都被挂起。此时,系统只能允许GC线程进行运行,其他线程则会全部暂停,等待GC线程执行完毕后才能再次运行。jvm虚拟机调优的目的:尽量避免full gc

GC的过程

  • 垃圾对象:没有被引用的对象,没有GCRoots链条上的对象
  • 非垃圾对象:被引用的对象

gc java roots详解 jvm gc root_垃圾收集器

可达性分析算法

难点:

  • 查找的过程,光方法区就常有数百上千兆,逐个检查效率太低
  • 查找的过程,是必须要暂停用户线程。

安全点(能说大概)

  • 来由:由于查找的过程需要暂停用户线程,不可能程序运行每条指令都执行一遍垃圾回收。
  • 定义:在特定的位置才会进行GC,这些位置就叫做安全点。
  • 选取原则:可以让程序一直安全的跑下去,不能让太多的死对象占据内存。
  • 遇到长时间执行的指令前就给它 GC 一下,类如方法的调用、循环跳转、异常跳转等,遇到类似的执行才会生成 Safepoint
  • 实现方式(这里的多线程是指运行同一份代码无法确定运行顺序的解决方案):
  • 抢占式中断:不管你现在在哪里,先给你中断,然后一看,噢原来你不在安全点,就恢复线程让你跑一会,然后又中断直到你到了安全点再 GC。(已经没有虚拟机会用这个)
  • 主动式中断:需要 GC 的时候,不需要强制中断线程了,只需要在安全点设置一个轮询标识,线程只需要去轮询这个标识即可,线程到安全点了,自己主动中断,进而 GC。

安全区域(能说大概)

针对主动式中断,假如我有一个线程正好block住,它没有在运行,就不能去轮询 GC 标识了,我们难道要等到它运行再 GC,不可能的对吧!

反过来想一下就是,假如线程本身就不再执行,那何必去管它呢,因为它不可能使引用发生变化啊。故我们又定义了一个安全区域的概念,在这个代码片段之中发生 GC 都是可以的,因为引用不曾改变。这就是扩大版的安全点啊,也就是它会把block的相关代码点设为安全区域

在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域,那样,当这段时间内 JVM 要发起 GC 时,就不管没到安全点但是在安全区域的线程。在线程要离开安全区域时,要检查系统是否已经完成了 GC,如果完成了,那就继续执行,否则就要等待 GC 结束的标识之后才可以离开安全区域。

记忆集(能说大概)

学会卡表和写屏障的概念

如果发现老年代的内存区域存在新生代的引用,那么将会将对象加入记忆集。垃圾回收时,记忆集里面的对象会加入GCroot,就避免了老年代的全遍历回收。

记忆集的实现方式是通过 卡表的方式

gc java roots详解 jvm gc root_用户线程_02

  • card Table:值01,若为1说明该卡页内对象存在指向收集区域的指针
  • card Page:存地址值
  • 映射关系:card Table[address>>9] 类似hashmap

何时记录变脏呢?

在引用类型字段赋值的那一刻,通过机器码层面的手段,把维护卡表的动作放到每一个赋值操作之中。也叫写屏障

并发可达性分析(必备)

根节点枚举所带来的停顿是非常短暂且相对固定的,但是GC Roots往下遍历对象图,这一步骤的时间与堆容量成正比,因此采用并发扫描。然而并发扫描存在并发问题如下:

垃圾收集器从 GC Roots 开始标记的过程示意图如下:

gc java roots详解 jvm gc root_gc java roots详解_03

三色标记(也可能会考):

  • 白色:对象尚未被垃圾收集器访问过(若在分析结束后,对象仍为白色,则表示不可达)
  • 黑色:对象已被垃圾收集器访问过,且该对象所有引用都已被扫描(安全存活的)
  • 灰色:对象已被垃圾收集器访问过,但未扫描完所有引用(即该对象正在被扫描,可理解为中间态)

注意引用是有方向的。

但是,如果在标记过程中,用户线程对引用关系做了修改,如下:

gc java roots详解 jvm gc root_赋值_04

在上图的(4)中:

  1. 原先对象 A 未引用 C,对象 B 引用了 C;
  2. 但标记到 B 时,用户线程断开了 B 到 C 的引用,而使 A 引用了 C;
  3. 则垃圾收集器标记完成后,C 依然是白色(即会被回收掉);
  4. 对象 DEFG 同理。

这样导致的后果就是:正在被对象 A 和 D 引用的对象 C 和 G,在垃圾收集器标记的过程中,由于用户线程的运行,导致本应存活的对象被垃圾收集器标记为消亡、并回收了。程序会因此报错,这是个严重的问题。

如何解决对象消失(主要掌握这个)

如何解决上述“对象消失”的问题呢?理论证明,当且仅当以下两个条件同时满足时,才会产生“对象消失”的问题:

  1. 赋值器插入了一条或多条从黑色对象到白色对象的新引用;
  2. 赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

针对这两个条件,在上图中,以对象 A、B、C 为例解释如下:
若只增加了 A 对 C 的引用,则 C 在垃圾回收后依然是存活的,不会出错。
若只有 B 断开了对 A 的引用,则 C 在垃圾回收后是消亡的,但并没有 A 对 C 的引用,因此也不会出错。

因此,要解决并发扫描时的对象消失问题,只需破坏其中一个即可。由此产生了两种解决方案:增量更新(Increment Update)和原始快照(Snapshot At The Begining, SATB)。

增量更新

  • 思路:破坏第一个条件。
  • 做法:黑色对象(A)插入新的指向白色对象(C)的引用关系(A→C)时,就将这个新插入的引用记录下来,待并发扫描结束之后,再以这些记录过的引用关系中的黑色为根,重新扫描一次。
  • 简化理解:黑色对象一旦新插入了指向白色对象的引用,它就变为灰色(需重新扫描)了。

原始快照

  • 思路:破坏第二个条件。
  • 做法:当灰色对象(B)要删除指向白色对象(C)的引用关系(B→C)时,就将这个要删除的引用记录下来,并发扫描结束后,再以这些记录过的引用关系中的灰色对象为根,重新扫描一次(可能会有一些垃圾对象但是也没关系)。
  • 简化理解:无论引用关系删除与否,都会按照刚开始扫描那一刻的对象图快照来进行搜索。

这两种方案都有在用:在 HotSpot 虚拟机中,CMS 是基于增量更新来做并发标记的,G1、Shenandoah 则是用原始快照实现的。

举例

以上图为例:在并发扫描时,增加了 A→C 引用,并且删除了 B→C 引用,若不采取任何措施,则扫描结束后对象 C 会消失。

两种解决方案的做法分别如下:

  • 增量更新:将已标记为黑色的对象 A 置为灰色,待并发扫描结束后,重新扫描对象 A。此时可以扫描到 A→C 引用,对象 C 不会消失。
  • 原始快照:若要删除 B→C 引用,则将原始的 B→C 引用记录下来(原始的快照),待并发扫描结束后,重新扫描对象 B,由于记录的是原始信息,其中包含 B→C 引用。这样,即便未扫描到 A→C 引用,对象 C 也不会消失。

此外,无论引用关系记录的插入还是删除,虚拟机都是通过写屏障实现的