可达性分析可以分为两个阶段Rootnodeenumeration从根节点开始遍历objectgraph在上一篇介绍垃圾回收算法的时候,我们简单介绍了下提到:标记紧凑算法(Mark-Compact)中移动幸存对象的操作是一个极其繁重的操作,整个过程必须暂停用户应用程序。这样的停顿被最初的虚拟机设计者形象地描述为“StopTheWorld(STW)”。显然STW不是什么好东西,如果可以避免的话还是需要尽量避免。在可达性分析中,第一阶段“可达性分析”必须是STW,第二阶段“从根节点开始遍历对象图”如果不进行STW会出现一些问题,因为第二阶段的时间比较长,而长时间的STW会影响性能,所以大佬们设计了一些解决方案,让secondstage不用STW也能用,大大减少了时间。首先,让我们做一个大概的介绍。大家对可达性分析的整体脉络有所了解就足够了。我将在下面详细解释。我会分两篇文章来写。本文将首先分析第一阶段“可访问性分析”!根节点枚举到目前为止,所有收集器在根节点枚举步骤中必须挂起用户线程,并且枚举过程必须在能够保证“一致性”的快照中进行.通俗的说,整个枚举过程中整个系统看起来像是冻结在了某个时间点,不会出现在分析过程中,用户进程还在运行,导致根节点的对象引用关系collection还在变化如果不能满足这几点,accessibility分析结果的准确性显然得不到保证,也就是说根节点枚举会面临类似StopTheWorld的麻烦,就像在Mark-Compact算法(Mark-Compact)我们前面提到过,另外,众所周知,能够作为GCRoots的对象引用只有少数,主要在global引用(例如常量或类静态属性)和执行上下文(例如虚拟机堆栈中引用的对象),虽然目标非常明确。但要使搜索过程快速高效并不是一件容易的事。如今,Java应用程序越来越大。光是方法区的大小就动辄上百GB,里面有很多类和常量。扫描检查所有这些区域显然太麻烦了。有什么办法可以减少耗时吗?一个很自然的想法,空间换时间!将引用类型及其对应的位置信息记录在一个哈希表中,这样GC到来时可以直接读取哈希表。而不是逐个区域扫描。这就是Hotspot的实现方式。用于存储引用类型的数据结构称为OopMap。下图是在HotSpot虚拟机客户端模式下生成的String::hashCode()方法的本地代码。可以看到在0x026eb7a9处的call指令有一条OopMap记录,说明EBX寄存器和栈中的偏移量为16的内存区各有一个OopMap的引用,有效范围从call指令到0x026eb730(指令流起始位置)+142(OopMap记录的偏移量)=0x026eb7be,即hlt指令。说实话,看不懂这一段没关系,知道OopMap是这么个东西就行。SafePoint在OopMap的协助下,HotSpot可以快速完成根节点的枚举,但是一个很现实的问题随之而来:因为引用关系可能会发生变化,这会导致很多改变OopMap内容的指令,如果相应的OopMap是为每条指令生成的,它会需要大量额外的存储空间,这样伴随垃圾回收而来的空间成本就会变得高得无法承受。所以实际上,HotSpot并不会为每条指令生成OopMap,而只是在“特定位置”生成OopMap。也就是说,只有在某些“特定位置”才会记录对象引用的相关信息,这些位置也称为安全点。通过安全点的设置,确定了用户程序在执行过程中不能随时停止和启动GC,但是强制要求程序必须执行到安全点才能进行GC(因为没有到达安全点(如果没有OopMap,虚拟机无法快速知道对象引用的位置,也无法枚举根节点)。如下图所示:因此,安全点的设置既不能太小使垃圾回收器等待太久,也不能太大导致垃圾回收频繁,会大大增加运行时的内存负载。因此,安全点的选择基本以“是否具有允许程序长时间执行的特性”为标准,最典型的是指令序列的复用:如方法调用、循环跳转、和异常跳转等,因此只有具有这些功能的指令才会生成安全点。关于安全点,还有一个需要考虑的问题就是GC发生时,如何让所有用户线程执行到最近的安全点,然后停止?这里有两种选择:PreemptiveSuspension:这个思路很简单,就是当GC发生的时候,系统首先中断所有的用户线程。然后,如果发现用户线程被中断的位置不在安全点,则继续执行这个线程,直到到达安全点,然后再次中断。抢占式中断最大的问题是不可控的时间成本,导致性能不稳定和吞吐量波动,尤其是在高并发场景下,这是非常致命的,所以现在几乎没有虚拟机实现使用抢占式中断来暂停线程响应GC事件.VoluntarySuspension:VoluntarySuspension不会直接中断线程,而是全局设置一个标志。用户线程会不断轮询这个标志。当发现标志为真时,线程将在最近的一个。安全点活动中断挂起。现在的虚拟机基本都是采用这种方式。SafeRegion安全点机制保证了程序在执行时,会在不太长的时间内遇到一个可以进入垃圾回收过程的安全点。对于主动中断,用户线程需要不断轮询标志位。处于睡眠或阻塞状态的线程(不活动的线程)怎么办?这些不活跃的线程得不到CPU时间,没有办法轮询标志位,自然也就没有办法找到最近的安全点主动中断挂掉。也就是说,对于这些不活跃的线程,我们无法控制它们什么时候醒来。很有可能其他线程已经通过轮询标志位到达安全点被中断,然后虚拟机开始根节点枚举(根节点枚举需要挂起所有用户线程),但是此时那些不活跃的用户线程被唤醒并开始执行,破坏对象之间的引用关系,那显然是不能接受的。对于这种情况,需要引入安全区域(SafeRegion)来解决。安全区的定义是这样的:保证在某段代码中,引用关系不会发生变化,所以在这个区域的任何地方启动GC都是安全的。安全区可以简单的看成是一个拉长的安全点。当用户线程执行到安全区的代码时,会先标记自己进入了安全区。这样,当虚拟机在这段时间要发起GC时,就不用担心安全区内的这些线程了。当安全区的线程被唤醒离开安全区时,需要检查主动中断策略的标志位是否为真(虚拟机是否处于STW状态),如果为真,则会继续挂起等待(防止根节点枚举提升过程中这些被唤醒的线程的执行破坏了对象间的引用关系),如果为false则标志还没有开始STW或者STW刚刚结束,那么线程就可以被唤醒,然后继续执行。
