参考:https://zhuanlan.zhihu.com/p/125765937
https://zhuanlan.zhihu.com/p/340530051
https://zhuanlan.zhihu.com/p/389896597

1.对象死亡判断


1.1引用计数(循环引用问题)

1.2可达性分析

2.垃圾收集算法


2.1 标记复制

2.2 标记-清除

2.3 标记-整理

3.HotSpot算法细节


3.1 GC Root枚举

当所有线程停下来的时候,并不需要一个不漏的检查完所有执行上下文和全局引用位置,虚拟机应该是有办法直接知道哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap的数据结构来达到目的的。

OopMap存储两种对象引用:
1、对象内的引用
在类加载完的时候,HotSpot就把对象内什么偏移量上是什么类型的数据计算出来。
2、栈、寄存器中引用
在JIT编辑过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。这样,GC在扫描的时候就知道这些信息了。通过OopMap垃圾收集器就可以更快的找到GC Roots,并且更快的完成GC Roots的枚举,大概展示如下图:image.png

3.2 线程中断点:安全点(Safe Point)

安全点是程序能够停顿的位置。即程序不是在任何时候停顿下来进行GC,只有到了安全点才去更新OopMap和停顿,等待GC完成在继续执行。

1.安全点设置目的
有了OopMap,HotSpot就能很快的完成GC Roots的枚举。但是每一行代码都有可能使引用变化,就需要更新OopMap,如果每一行都执行一次OopMap更新,肯定是不科学的,所以就有了安全点(safe point)。
2.安全点选择标准
是否就有让程序长时间执行的特征。一条指令执行时间都很短,而一段程序一般不会说因为很长的指令流而造成长时间的运行,所以一般都是在指令复用的地方出现。比如:方法调用、循环跳转、异常跳转
3.抢断式中断
在GC发生时,系统直接终端所有线程,如果发现没有在安全点的线程,再恢复该线程让他跑到安全点。现在几乎没有虚拟机采用这种方式。
4.主动式中断
当GC需要中断线程时,设置一个标志,各个线程去轮询这个标志,发现需要中断,线程就自己中断。轮询点和安全点在一个地方,在加上创建对象需要分配内存的地方。实现方式:设置一个内存不可读,当线程访问这个内存就会产生一个自陷异常信号,预先注册的异常处理器中捕获这个异常暂停线程。通过一个指令和一个异常处理器就实现了这个功能。

3.3 安全区域(Safe Region)

安全点解决了正在执行的线程中断问题,我们知道线程还有没执行的状态,比如线程是Sleep、Blocked状态。这些线程不能自己走到安全点。如果休眠的线程在GC途中醒来,在线程运行到安全点之前就会有可能修改对象的引用关系。所以我们需要在线程醒来的时候如果正在GC那么也中断。

安全区域就是在一段代码中引用关系不会发生变化。所以在这个区域内任何地方GC都是安全的。在执行到安全区的时线程会标识自己处于安全区中,当离开安全区时,就需要检查系统是否已经完成枚举GC Roots(或者整个GC过程),如果已经完成那么线程继续执行,否则就等待。

线程的Sleep、Blocked(这个区域内当前线程肯定不会改变对象引用)就被包含在安全区中,也就是说只要线程Sleep那么他就处于安全区,一旦Sleep时间到线程继续执行,首先就要判断是否能够离开安全区。
image.png

3.4 记忆集和卡表

1.记忆集

为了解决对象跨代引用的问题,垃圾回收器在新生代建立记忆集的数据结构。记忆集将老年代划分为若干小块,标识出那一块区域存在跨代引用。发生Minor GC时,只需要扫描包含跨代引用的老年代区域,将其加入GC Root中

2.卡表

卡表是记忆集的一种具体实现形式

image.png
一个卡页的内存中通常包含不止一个对象,只要卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为1,成这个元素变脏(Dirty),没有则标识为0。在垃圾收集发生时,只要筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中包含跨代指针,把他们加入GC Roots中一并扫描。

3.5 可达性分析-三色标记

从GC Roots开始遍历,可达的就是存活,不可达的就回收。
image.png
标记的过程大致如下:

  1. 刚开始,所有的对象都是白色,没有被访问。
  2. 将GC Roots直接关联的对象置为灰色。
  3. 遍历灰色对象的所有引用,灰色对象本身置为黑色,引用置为灰色。
  4. 重复步骤3,直到没有灰色对象为止。
  5. 结束时,黑色对象存活,白色对象回收。

这个过程正确执行的前提是没有其他线程改变对象间的引用关系,然而,并发标记的过程中,用户线程仍在运行,因此就会产生漏标错标的情况。

错标只有在满足下面两种情况下才会发生:
image.png
只要打破任一条件,就可以解决错标的问题。
原始快照打破的是第一个条件:当灰色对象指向白色对象的引用被断开时,就将这条引用关系记录下来。当扫描结束后,再以这些灰色对象为根,重新扫描一次。相当于无论引用关系是否删除,都会按照刚开始扫描时那一瞬间的对象图快照来扫描。
增量更新打破的是第二个条件:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。相当于黑色对象一旦建立了指向白色对象的引用,就会变为灰色对象。


4.垃圾回收器


4.1 CMS

image.png
1、初试标记
初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快。初始标记的过程是需要触发STW的,不过这个过程非常快,而且初试标记的耗时不会因为堆空间的变大而变慢,是可控的,因此可以忽略这个过程导致的短暂停顿。
2、并发标记
并发标记就是将初始标记的对象进行深度遍历,以这些对象为根,遍历整个对象图,这个过程耗时较长,而且标记的时间会随着堆空间的变大而变长。不过好在这个过程是不会触发STW的,用户线程仍然可以工作,程序依然可以响应,只是程序的性能会受到一点影响。因为GC线程会占用一定的CPU和系统资源,对处理器比较敏感。CMS默认开启的GC线程数是:(CPU核心数+3)/4,当CPU核心数超过4个时,GC线程会占用不到25%的CPU资源,如果CPU数不足4个,GC线程对程序的影响就会非常大,导致程序的性能大幅降低。
3、重新标记
由于并发标记时,用户线程仍在运行,这意味着并发标记期间,用户线程有可能改变了对象间的引用关系,可能会发生两种情况:一种是原本不能被回收的对象,现在可以被回收了,另一种是原本可以被回收的对象,现在不能被回收了。针对这两种情况,CMS需要暂停用户线程,进行一次重新标记。
4、并发清理
重新标记完成后,就可以并发清理了。这个过程耗时也比较长,且清理的开销会随着堆空间的变大而变大。不过好在这个过程也是不需要STW的,用户线程依然可以正常运行,程序不会卡顿,不过和并发标记一样,清理时GC线程依然要占用一定的CPU和系统资源,会导致程序的性能降低。

4.2 G1

image.png
G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆空间划分为多个大小相等的独立区域(Region),每个Region都可以成为 Eden空间、Survivor空间、老年代空间。

这种思想上的转变和设计,使得G1可以面向堆内存任何部分来组成回收集来进行回收,衡量标准不再是它属于哪个分代,而是哪块内存存放的垃圾最多,回收收益最大,这就是G1收集器的 Mixed GC模式,即混合GC模式。

Region还有一类特殊的 Humongous 区域,专门用来存储大对象。G1认为只要大小超过了一个Region容量一半的对象即可判定为大对象。如果是那些超过了整个Region容量的超大对象,将会放在连续 N 个 Humongous Region区域。

G1的记忆集可以理解为一个哈希表,Key就是别的Region的起始地址,Value就是卡表的索引号集合。
image.png

1.初始标记(Initial Marking):这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中创建新对象,这阶段需要停顿线程,但是耗时很短。而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿。
2.并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,但是可以与用户程序并发执行。当对象图扫描完成以后,还要重新处理SATB记录下的在并发时有引用变动的对象。
3.最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录。
4.筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划。可以自由选择多个Region来构成会收集,然后把回收的那一部分Region中的存活对象==复制==到空的Region中,在对那些Region进行清空。

除了并发标记外,其余过程都要 STW

4.3 CMS VS G1

  1. G1从整体上来看是 标记-整理 算法,但从局部(两个Region之间)是复制算法。而CMS是 标记-清除算法 所以说,G1不会产生内存碎片,而CMS会产生内存碎片
  2. CMS使用了 写后屏障来维护卡表,而G1不仅使用了写后屏障来维护卡表,还是用了 写前屏障来跟踪并发时的指针变化情况(为了实现原始快照)。
  3. CMS对Java堆内存使用的是传统的 新生代和老年代划分方法,而G1使用的全新的划分方法。
  4. CMS收集器只收集老年代,可以配合新生代的Serial和ParNew收集器一起使用。G1收集器收集范围是老年代和新生代。不需要结合其他收集器使用
  5. CMS使用 增量更新解决并发标记下出现的错误标记问题,而G1使用原始快照解决