垃圾回收算法

Java虚拟机涉及的gc回收器细节和选择上是较为复杂的,本篇文章主要讲解gc回收器有哪些基本算法以及如何进行选择的。垃圾回收算法主要针对java运行时数据区域的堆区域。

垃圾回收算法

可达性分析算法

可达性分析算法是当前主流语言c#,java等通用的判定对象是否存活的算法。这个算法通过一系列的Gc Roots的对象作为起点,从这些结点开始进行可达性分析,从图论的角度探究当一个对象到根对象不可达时那么可以视为死亡。可达性分析算法是一个安全的垃圾回收算法,因为可以防止对象之间互相引用的对象群也能被彻底回收。当前主流算法包括swift语言都采用引用计数法进行判定对象的生存状态,减少了维护树结构的成本但是可能有对象会逃逸。Gc Roots:

  • Green 虚拟机栈(栈帧中的局部变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中Native方法引用的对象。

Gc Roots是java垃圾回收之前必须要停顿进行遍历的,这个过程是Stop The World的,也就是所有线程都停顿在安全点,用户感觉到的是响应时间加长。如果是并发回收在并发标记结束之后还需要进行重新标记,因为Roots在并发过程中是在变化的。

标记-清除算法(Mark-Sweep)

内存回收需要经过标记阶段标记死亡对象,然后通过清除将对应的内存地址回收,这个过程是回收算法最基本的过程,几乎每个算法都需要经历这个过程。但是标记清除算法在空间和时间上都不算优秀,时间上标记和清除两个过程效率都不高,空间上来说会造成大量的内存碎片,在大对象到来的时候如果碎片化比较严重会造成无法分配内存地址提前触发gc,但是可能实际还有较大比例的内存剩余。

复制-清除算法(Mark-Copy-Sweep)

复制清除算法为了解决效率问题提出的,将可用内存分为两块,使用的时候只使用其中的一块,回收的时候直接将存活的对象复制到另外一块区域,同时将回收区域全部清空。这种算法解决了空间碎片的问题,因为复制过程中都是有序的进行指针的移动。同时也提高了清除的时间效率,全部清除即可。这种方法的缺陷是分成两块减少了内存利用率,同时如果存活对象过多复制成本很高,所以针对存活率较低的区域可以使用,这里参考新生代回收设计:
cmd-markdown-logo

标记-整理-清除算法(Mark-Compact-Sweep)

复制算法在对象生存周期比较长回收率比较低的情况下复制量大效率很低,同时还需要维护双倍的内存空间,在新生代利用比较合适。但是针对回收率低的采用整理算法,这种算法会先进行标记,然后将存活的对象向头部进行移动,然后直接删除存活边界外也就是死亡的对象,整理的过程代价较大需要进行频繁的指针移动。

分代收集算法

为什么要采用分代收集,我们回顾了上述算法的设计,其实并没有一种完美的算法,算法都是基于场景设计的。分代收集能够很好的处理多种情况,如果是回收量多,大批死去只有少量存活那么复制算法是最高效的,如果大量存活那么整理算法更合适。如果不考虑碎片带来的影响,那么标记清除时最快速的。CMS收集器就采用了标记清除算法,可以通过Tomcat启动参数配置老年代采用该收集器,也可以设定开启整理,通过标记整理清除方式的CMS收集器维护老年代。

HotSpot算法实现

垃圾回收算法在细节上的实现并没有上面描述的这么简单,在运行过程中,即使是确定Gc Roots都是一个极为复杂的过程,何时中断所有线程,如何根据根结点快速遍历出整个回收树结构也是效率上需要考虑的。

枚举根节点

枚举根节点是必须进行停顿的,中断当前正在执行的所有线程。如果在找到Gc Roots的前提下如果要逐个检查方法区中的引用,会消耗很多的时间,所以维护一个准确的位置时虚拟机所需要考虑的。

OopMap

当执行系统停顿下来之后,虚拟机通过OopMap可以直接得知哪些地方存放着对象引用。这个Map是在类加载完的时候,HotSpot虚拟机就把对象什么偏移量上是什么类型的数据都计算出来,在JIT编译过程中,在特定位置记录下栈和寄存器哪些位置是引用。这样在gc扫描的时候就可以直接得知这些信息而不需要进行全局扫描。

安全点(Safepoint)

如果为每条指令都生成OopMap,那么程序在每个地方都能暂停进行垃圾回收,但是这样做的空间成本太大,所以虚拟机涉及会在一些地方生成OopMap,这样的地方称之为安全点,程序需要到达这些地方才可以进行线程中断开始标记过程。

安全点的选择

安全点的选择主要需要平衡gc的等待时间和运行负荷,所以一般如果调用的栈帧比较长那么会在进入栈帧之前设立一个安全点,所以一般如方法调用,循环跳转,异常跳转等指令之前会生成safepoint。

抢先式中断和主动式中断

虚拟机发出gc指令时,程序需要跑到最近的安全点上才能进行标记过程,那么线程如何跑到各自的安全点上去响应中断。

  • 抢先式中断在gc发生时,首先把所有线程中断,如果线程中断的地方不在安全点上,就恢复线程,让它继续跑到安全点上。现在基本是采用抢占式中断来暂停线程响应gc事件,不需要线程的执行代码主动去配合,虚拟机线程调度可以完成。
  • 主动式中断的思想是当gc需要中断线程时,不直接对线程进行操作,而是设置一个标志,各个线程执行的时候主动去轮询这个标志,发现中断标志为真的时候就中断挂起,轮询标志的地方和安全点是重合的。

安全区域

安全点机制保证了多个线程能够响应gc同时进入安全点之后中断挂起,但是如果线程当前处于sleep或者blocked状态,这时候线程时无法响应jvm的中断请求的,jvm也不可能去等待cpu再次分配时间片调度这些线程。所以有了安全区域(Safe Region),在这个区域的任何一个点开始gc都是安全的。可以把这个区域看成是扩展了的Safepoint。

总结

本篇主要分享了各种垃圾回收算法的设计和利弊,以及在实际运行过程中是如何通过安全点和安全区域提高gc回收效率的。这对之后各种回收器的理解是非常重要的,也为调优提供了理论基础。