1、垃圾收集算法
垃圾收集算法的实现涉及大量的程序细节,而且各个平台的虚拟机操作内存的方法又各不相同,介绍几种垃圾收集算法的思想及其发展过程.
1.1、标记-清除算法
垃圾收集分为“标记” 和“清除”两个阶段:首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。说它是最基础的收集算法,其他收集算法都是基于这种思路并对其不足进行改进而得到的. 它的主要不足有两个:
- 效率问题,标记和消除两个过程的效率都不高:
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
算法执行过程如下图所示:
1.2、复制算法
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。实现简单,运行高效。
算法执行过程如下图所示:
1.3、标记-整理算法
首先标记所有需要回收的对象,然后让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
算法执行过程如下图所示。
1.4、分代收集算法
商业虚拟机的垃圾收集都采用“分代收集”( Generational Collection )算法,根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代初老年代, 这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去, 只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。
算法执行过程如下图所示。
2、Hotspot算法实现存活对象判断和垃圾收集
2.1、枚举根节点
作为GC Roots 的节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,它们的数量庞大,逐个检查里面的引用,会消耗很多时间。其次,准确的可达性分析需要暂停用线程的执行。
目前的主流Java 虚拟机使用的都是准确式GC (准确式内存管理Exact Memory Management:虚拟机知道内存中某个位置的数据类型具体是什么),所以当执行系统停顿下米后, 并不需要一个不漏地检验所有执行上下文和全局的引用位置,虚拟机有办法直接得知哪些地方存放着对象引用。在HotSpot 的实现中, 使用一组称为OopMap 的数据结构来实现这个目的,在类加载完成的时候, HotSpot 就把对象内,什么偏移量上是什么类型的数据计算出来,在JIT 编译过程中,也会在特定的位置记录下战和号寄存器中哪些位置是引用。GC 在扫描时就可以直接知道这些信息了。
2.2 安全点
为每条指令都生成OopMap,消耗大量空间,GC成本很高。HotSpot也没有这样做,只在特定位记录这些信息,这些位置被称为“安全点”,即程序执行时并非所有地方都可以停下来开始GC,只有在到达安全点时才能暂停。安全点的选择是以程序“是否具有让程序长时间执行的特征”了标准进行选定的。最明显的就是指令复用,如方法调用,循环跳转。
如何让所有线程(不包括执行JNI 调用的线程)都“ 跑”到最近的安全点上再停顿下来。有两种方案抢先式中断( Preemptive Suspension )和主动式中断( Voluntary Suspension )
-
-
抢先式中断:不需要线程的执行代码主动去配合,在GC 发生时,首先把所有线程全部中断,如果发现有钱程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上. 现在几乎没有虚拟机实现。
-
主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现标志为真时就自己中断挂起。
-
轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
2.3 安全区域
命案点解决了如何进入GC 的问题,问题还是存在,如果线程处于睡眠状态或者阻塞状态,此时线程无法响应JVM的中断请求,不能“走”到安全的地方去中断挂起。 JVM 也不可能等待线程重新被分配CPU 时间。这样就需要要安全区域( Safe Region )来解决。
安全区域:指在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC 都是安全的,可以把安全区域看做是被扩展了的安全点。在线程执行到安全区域中的代码时,首先标识自己已经进入了安全区域,这段时间内JVM 发起GC,JVM不用管标识为安全区域状态的钱程了。线程离开安全区域时,它要检查系统是否已经完成了根节点枚举(或者是整个GC 过程),如果完成了,线程就继续执行,否则就必须等待直到收到可以安全离开安全区域的信号。