JVM—HotSpot GC算法实现

1.枚举根节点

之前说的寻找堆中"垃圾"内存的方法是使用可达性分析算法,用一系列的GC Roots去找到引用链,进而找到已经没有用的对象,但其实现起来有两个要注意的地方

  • 1.GC Roots往往选取全局性的引用(方法区中的常量或静态属性)和执行上下文(虚拟机栈或本地方法栈中的本地变量表),而很多应用仅仅方法区就有数百兆,如果逐个检查其中的引用,会很耗费时间
  • 2.GC停顿: 在执行可达性分析时,不可以出现分析过程中对象引用还在不断变化的情况,否则分析结果的准确性就无法保证,故GC进行时必须停顿所有线程(“Stop The World”)

解决办法:准确式GC,也就是说,当执行系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机有办法直接知道哪些地方存放着对象引用. 在HotSpot中,使用一组称为OopMap的数据结构来达到这个目的. 在类加载完得到时候, HotSpot就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中的哪些位置是引用,这样,在GC扫描的时候就可以直接得知这些信息了

2.安全点和安全区域

在OopMap的帮助下,HotSpot可以快速且准确完成GC Roots枚举,但是如果为每一条指令都生成对应的OopMap,会消耗大量的额外空间,这样的话GC的成本就会太高

实际上,并不是每条指令都有对应的OopMap, 只是在"特定的位置"记录了这些信息,这些位置称为"安全点(SafePoint)",也就是说程序不是在任何地方都可以停下来开始GC,只有到安全点的时候才能暂停. 所以我们一般选取"能让程序长时间执行"的指令作为GC时的安全点,最明显的就是指令序列复用类的指令,比如方法调用,循环跳转,异常跳转等.

对于SafePoint,还有一个需要解决的问题: 如何在GC发生时让所有线程都跑到最近的安全点再停顿下来?解决办法有两个

  • 抢先式中断:GC发生时,首先中断所有线程,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点
  • 主动式中断:当GC需要中断线程的时候,不直接对线程进行操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮训标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方

SafePoint保证了程序在执行时,在不太长的时间内就会遇到可以进入GC的SafePoint,但如果程序"不执行呢"? 也就是如果没有给程序分配CPU时间,线程处于挂起或阻塞状态,这时候线程无法响应JVM的中断请求,这就需要用到安全区域(Safe Region)来解决

安全区域就是指: 在一段代码片段中, 引用关系不会发生变化,在这个区域任意地方开始GC都是安全的

当线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region, 这样当JVM开始GC时就不用管标识自己为Safe Region状态的线程了, 当线程要离开Safe Region的时候,它要检查系统是否已经完成了根节点枚举(或是整个GC过程),如果完成了,那线程就继续执行,否则就等待直到收到可以安全离开Safe Region 的信号为止

3.垃圾收集器

在这里插入图片描述

1>Serial收集器

是一个单线程的收集器,使用一条收集线程去完成垃圾收集工作,并且在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束.因为简单而高效,目前仍然是虚拟机运行在Client模式下默认的新生代收集器(采用复制算法)

在这里插入图片描述

2>ParNew收集器

其实就是Serial收集器的多线程版本,是运行在Server模式下的首选的新生代收集器,在单CPU下不一定比Serial收集器效率高,但随着CPU数量增加,对于GC时系统资源的利用是有好处的(采用复制算法)

3>Parallel Scavenge收集器

也是一个并行的多线程收集器,但与ParNew收集器不同的是: 关注点与其他收集器不同,比如CMS收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,其实就是运行用户代码与CPU消耗时间的比值,也就是高效的利用CPU的时间,尽快完成程序的运算任务.

可以通过设置参数来控制最大垃圾收集停顿时间吞吐量大小

还有一个开关参数,这个参数打开后,不需要手动指定新生代的大小,Eden与Survivor区的比例,晋升老年代的对象大小等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数并提供最合适的停顿时间或者最大的吞吐量, 这种机制叫做GC的自适应调节策略,自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的重要区别

4>Serial Old收集器

是Serial收集器的老年代版本,使用"标识-整理"算法,在Client模式下的虚拟机使用; 在Server模式下,可以作为CMS收集器的后备预案,在并发收集时发生Concurrent Mode Failure时使用

5>Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用"标记-整理算法",配合Parallel Scavenge收集器,在注重吞吐量以及CPU资源敏感的场合,可以应用

6>CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,是基于"标记-清楚"算法实现的,大致过程包括:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清除

其中,初始标记和重新标记仍然需要中断所有程序执行线程,初始标记仅仅只是标记一下能被GC Roots直接关联到的对象,速度很快;并发标记阶段就是GC Roots Tracing的过程; 而重新标记是为了修正并发标记阶段引用用户程序继续执行而导致标志产生变化的一部分的标记记录

CMS收集器优点: 并发收集,低停顿

缺点:

  • CMS对CPU资源非常敏感,
  • 无法处理浮动垃圾,所谓浮动垃圾就是在并发清除阶段,伴随程序运行还会不断有新的垃圾不断产生,但这部分垃圾并没有被标记,所以只好留在下一次GC时清理,也就是说,每次还要预留有足够的内存空间给用户线程使用. 要是预留的内存无法满足程序需要,就会出现一次"Concurrent Mode Faliure"失败,这时虚拟机将启动后备预案: 临时启动Serial Old收集器重新进行老年代的垃圾收集,这样停顿时间就很长了
  • 由于基于"标记-清除"算法,会产生大量的内存碎片,会给大对象分配带来麻烦,为了解决这个问题,CMS收集器提供了一个默认开启的开关参数, 用于CMS收集器在顶不住要进行FullGC时,开启内存碎片的合并整理过程,内存整理是不可以并发的,所以停顿时间会延长, 还有一个参数可以设置执行多少次不压缩的FullGC后,跟着来一次带压缩的,默认为0

7>G1收集器

G1,即"Garbage First"收集器,与其他收集器相比,有以下优点

  • 并行与并发,缩短用户现成的停顿时间
  • 分代收集,采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的就对象以获取更好的收集效果
  • 空间整合:整体是基于"标记-整理",但局部看是基于"复制"算法实现的,意味着运行期间不会产生内存空间碎片,收集后都能提供规整的可用内存
  • 可预测的停顿:建立可预测的停顿时间模型,明确在一段时间片中,GC时间不得超过N毫秒

G1将堆分为若干个大小相等的独立区域(Region),牺牲东戴河老年代不再是物理隔离的了,他们都是一部分Region的集合

G1跟踪各个Region中垃圾堆积的价值大小,在后台维护一个优先列表,优先回收价值最大的Region

但一个对象分配在某个Region当中,它还可能被其他Region中的其他对象引用,这点虚拟机使用Remembered Set来避免全堆扫描,每个Region都会有对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中,如果是,那就通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中. 这样在进行内存回收时,在GC根节点的枚举范围内加上Remembered Set就可以保证不需要进行全堆扫描

G1收集器的步骤

  • 初始标记:值标记一下GC Roots能直接关联到的对象
  • 并发标记:对堆中对象进行可达性分析,找出存活的对象,耗时较长
  • 最终回收:修正在并发标记时因用户程序继续运作而导致标记产生变化的那一部分标记记录
  • 筛选回收:对各个Region的回收价值和成本进行排序,根据用户期望的GC停顿时间指定回收计划

猜你喜欢

转载自blog.csdn.net/wintershii/article/details/89295500