码字不易,欢迎大家转载,烦请注明出处;谢谢配合

之前介绍了利用可达性分析法来分析对象到GC Roots是否存在引用链,那么HotSpot是如何实现的呢?

HotSpot算法实现

枚举根节点

HotSpot是如何获取根节点的呢?我们之前提到Java虚拟机栈中引用的对象,JNI方法中引用的对象,方法区中的类的静态属性引用的对象,方法区中常量引用的对象;在实际应用中仅方法区大小可能就达数百兆,通过逐一检查的方式会很耗时,显然不是很可取。

另外,可达性分析这项工作必须确保在一致性状态的快照中进行,“一致性”是指在分析的过程中对象的引用像冻结在某个时间点一样,不可以出现分析过程中,引用关系还在变化;所以便有了臭名昭著的“Stop The World”。

目前主流虚拟机采用准确式GC,所以当系统停顿下来时,并不需要逐一检查所有执行上下文和全局的引用位置;HotSpot实现是通过OopMap的数据结构达到此目的,在完成类加载的时候,把对象内什么偏移量上是什么数据类型计算出来,在JIT编译过程中,记录下栈和寄存器什么位置是引用;这样在GC扫描时就可以直接得到这些信息。

安全点 Safepoint

程序在执行时并非在所有的节点都能停顿下来开始GC,只有到达特点区域才能开始,这些特定区域称之为安全点(Safepoint);安全点的选择不能太少,否则将导致GC等待时间过长;也不能过于频繁,导致增大运行时负荷。安全点选定的基础是“是否具有让程序长时间执行的特征”,每条指令的执行速度都很快,因为指令流长度过长而导致“长时间”执行不太可能;一般“长时间”的最明显特征是指令复用,一把是方法调用,循环跳转,异常跳转等。

如何让所有进程都进入安全点停顿下来呢?有两种方式:抢断式中断,主动式中断;对于抢断式中断,是指抢在线程之前中断,这就可能导致有的线程还没有到达安全点,这就需要恢复线程,使其运行至安全点;其过程可能导致线程的中断-恢复-中断,线程消耗较大,所以大多数都没有采用这种方式;主动式中断,是指不许要主动操作线程,设定一个标志位,让线程主动轮询标志位,标志位为真,则主动中断至安全点。

安全区域 Safe Region

安全点解决了线程进入GC的问题,但是并不是完美的,例如线程处于Block或者Sleep状态时,无法响应中断指令,希望线程到达安全点中断不太可能,等待线程获取CPU资源在进入安全点也不太可能,这时就需要安全区域(Safe Region)来解决。

安全区域是指在一端代码片段中对象的引用关系不会发生变化,在这个区域的任意位置开始GC都是安全的,所以安全区域可以认为是安全点的扩展。

当线程执行到安全区域代码时,标注自己进入了安全区域;当线程想离开安全区域时,检查是否完成了根节点枚举,如果完成了则继续执行;如果没有完成,则需要等待接收到离开Safe Region的信号,才能离开。

垃圾收集器

前文介绍了垃圾收集算法,而垃圾收集器就是收集算法的实现,如下是HotSpot虚拟机常用的垃圾收集器的配合使用情况,连线表示可以配合使用:

image-1655134724002

Serial收集器

Serial收集器是最基本的,历史最悠久的收集器,曾经在JDK1.3之前它是新生代唯一的选择,它采用的是我们前面提到的复制算法;从它的名字就可以看出单线程的收集器,只能通过单个收集线程完成收集工作,更重要的是在收集的过程中,其他线程必须停止工作,直到垃圾收集完成。在单CPU的情况下Serial收集器简单高效,避免了线程交互的开销,Serial收集器对于运行Client模式下的虚拟机是个很好的选择。

垃圾收集带来的停顿带来不良的用户体验,虚拟机设计者表示可以理解,但是也表示很委屈;例如你在收拾屋子时,也会尽量让孩子待在某处;而不是你一边收拾,孩子一边制造垃圾。

image-1655134744782

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,同样的它也是复制算法的实现,重用了Serial收集器的大部分代码;不同的是它采用多条线程进行垃圾收集,同时提供了Serial收集器可用的控制参数,-XX:SurvivorRatio、-XX:PretenureSizeThreshold、XX:HandlePromotionFailure等;它是许多Server端虚拟机回收新生代的首选,其中一个与性能无关的原因是,除了Serial收集器,只有它能与CMS收集器配合使用。但是需要注意的是,在单CPU的情况下,ParNew收集器的效率绝对不会比Serial收集器更好,反而会因为线程之间的切换带来更多的开销。

image-1655134766358

Parallel收集器

Parallel收集器同ParNew垃圾收集器一样也是多线程对新生代进行垃圾收集,但与其不同的是Parallel收集器更关注控制吞吐量,而其他垃圾收集器更关注停顿时间;吞吐量的含义是指运行用户线程的时间与虚拟机总运行时间的占比;例如虚拟机总共运行了100分钟,而垃圾收集耗费了1分钟,那么吞吐量则是99%;

Parallel收集器提供了两个参数用于控制吞吐量;-XX:MaxGCPauseMillis设置停顿时间,-XX:GCTimeRatio则是直接控制吞吐量;但是这里需要注意的是,有的人以为我将-XX:MaxGCPauseMillis设置的很小是不是就可以减少停顿时间了呢?其实不然,XX:MaxGCPauseMillis可以减少停顿时间,但有可能导致GC频率加快;比如原本10分钟进行一次GC,每次停顿100ms;有可能变为1分钟进行一次GC,每次停顿70ms,其效果可能可想而知。

Serial Old收集器

Serial Old收集器是Serial收集器针对老年代的版本,它使用的是标记-整理算法,同样的也是更加适用于Client模式下虚拟机,主要有两大用途,一种是跟JDK1.5版本之前的Parallel收集器配合使用,另一种用途是CMS收集器的后备预案,在Current Mode Failure时使用。

Parallel Old收集器

Parallel Old收集器是Parallel收集器的老年代版本,它采用的是标记-整理算法,在JDK1.6之前 Parallel 收集器只能与Serial Old收集器配合使用,在多CPU的环境中Serial Old无法利用多CPU的优势,直到Parallel Old收集器出现以后,关注“吞吐量”的收集器终于能够配合使用;在关注吞吐量的场景,以及CPU敏感的环境中可以选择 Parallel 跟ParallelOld 收集器配合使用。

CMS收集器

CMS(Current Mark Sweep)收集器是采用标记-清除算法,它用于老年代的垃圾收集,它是真正的并发的收集器,它可以实现“孩子一边扔垃圾,父母一边收拾”;它是如何实现的呢?它的收集过程分为以下四个步骤:

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

初始标记是标记GC Roots 能直接关联的对象,而并发标记则是GC RootsTracing 的过程,至于重新标记则是用于重新标记并发标记过程中被用户线程修改的标记;所以初始标记,重新标记这两个过程仍然是有停顿的。但是并发标记与并发清除两个阶段可以与用户线程一起工作,所以多被应用于B/S架构的服务端

CMS收集器无法清理浮动垃圾,因为并发清除阶段同用户线程一同工作,所以可能有新的垃圾产生,所以可能导致“Current Mode Failure”,就需要我们使用前面提到的Serial Old 作为后备方案;除此之外,由于CMS收集器采用的是标记-清除算法,有可能产生我们我们之前在算法篇提到的不连续的内存空间,无法分配给需要较大的空间的对象,不得不触发新一次的Full GC。

image-1655134808809

G1收集器

G1(Garbage-First) 收集器,在JDK1.7正式商用,是当今最前沿的垃圾收集器之一,它采用的是复制算法与标记-整理算法的结合;同时其他收集器都收集的范围都是整个新生代或老年代,而G1则是将Java堆划分成多个大小相等的Region,虽然保留了新生代和老年代的概念,但它们不在是物理隔离的,都是一部分Region的集合。

G1相对于CMS收集器的另一个优势是可以建立可预测的时间模型;而它能够建立此模型的基础也正是由于避免了整个Java堆的垃圾收集,G1收集器会追踪每个Region垃圾收集的价值,建立一个优先列表,每次先回收最有价值的Region。

image-1655134825489

附录:垃圾收集器参数总结

参数 描述
UseSerialGC Client模式下虚拟机默认选择,打开后使用Serial+Serial Old组合使用
UseParNewGC 打开后使用ParNew+Serial Old 组合使用
UseConcMarkSweepGC 打开后使用ParNew+CMS+Serial Old 组合使用
UseParallelGC 打开后使用Parallel+Serial Old 组合使用
UseParallelOldGC 打开后使用Parallel+Parallel Old 组合使用
SurvivorRatio 设置新生代中Eden与Survivor比例,默认是8,即Eden:Survivor=8:1
PretenureSizeThreShold 设置直接晋升到老年代对象大小
MaxTenuringThreShold 设置晋升到老年代年龄,每进行一次Minor GC 年龄加1
UseAdaptiveSizePolicy 动态调整进入老年代的大小以及年龄
HandlePromotionFailure 是否允许分配担保失败
ParallelGCThreads 设置并行GC线程数
GCTimeRatio GC占总时间比例
MaxGCPauseMillis 设置GC最大停顿时间,仅在Parallel收集器使用时有效
CMSFullGCsBeforeCompaction 设置在若干次FullGC之后,进行内存碎片整理,仅针对CMS有效
UseCMSCompactAtFullGC 是否在FullGC之后进行内存碎片整理,仅针对CMS有效
CMSInitiatingOccupancyFraction 设置老年代使用多少比例之后触发垃圾回收,默认68%,仅针对CMS有效