垃圾回收算法

垃圾回收算法

Java虚拟机(JVM)垃圾回收器提供的一种用于在空闲时间不定时回收无任何对象引用的对象占据的内存空间的一种机制。

整个回收过程分为几个阶段:找出需要回收的对象 —> 对需要回收的对象进行标记 —> 回收对象内存

前面两个阶段采用的是 根搜索算法,回收对象的的算法包含1.基础的标记-清除算法。2进阶的标记-整理算法。3.高效的复制算法,在回收过程中会根据堆的使用情况来选择合适的算法。

根搜索算法

基本概念:通过遍历图中节点的方式(离散数学),从GC Roots出发,找出并标记可达节点(不需要的变量);其中GC Roots属于Root Set。

Root Set: 正在执行的Java程序可以访问的引用变量(注意:不是对象)的集合(包括局部变量、参数、类变量),程序可以使用引用变量访问对象的属性和调用对象的方法

算法思路:

  1. 通过一系列名为“GC Roots”的对象作为起始点,寻找对应的引用节点。

  2. 找到这些引用节点后,从这些节点开始向下继续寻找它们的引用节点。

  3. 重复(2)。

  4. 搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的。

在这个过程其中需要在意是什么对象是GC Roots,垃圾回收器对以下特殊的对象定义为GC Roots对象:

(1)虚拟机栈中引用的对象(栈帧中的本地变量表);

(2)方法区中的常量引用的对象;

(3)方法区中的类静态属性引用的对象;

(4)本地方法栈中JNI(Native方法)的引用对象。

(5)活跃线程。

gc_root_search

在标记阶段应用进程会先暂停以便于JVM进行垃圾回收,这种暂停情况叫做安全点,这会出发一次Stop The World(SWT)。但SWT时长并不取决于堆中对象的多少和堆的大小,而是存活对象的多少。

在根搜索算法中,要经过2次标记,才能决定哪些对象的内存空间真正被回收:

  • 对象A没有和任何GC Roots相连,它会被标记并进行筛选。筛选的标准是它的finalize()需要被执行不。
  • 如果被认为需要执行finalize(),对象A就会进入F-Queue中,然后由JVM自动创建的低优先级的Finalizer线程去执行finalize()释放空间;而第二次标记的机会就是在对象A的finalize()中把对象A重新连接到一个GC Roots的链上,如果成功了,对象A就会被标记为可达节点,不会被回收掉。

回收对象内存的算法

1 标记-清除算法

最基础的回收算法,基本就是根搜索算法进行标记出哪些对象不需要回收,哪些需要回收;在标记后统一回收所有被标记为回收的对象。

优点:不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活对象比较多的情况下极为高效。

缺点:(1)标记和清除过程的效率都不高。这种方法需要使用一个空闲列表来记录所有的空闲区域以及大小。对空闲列表的管理会增加分配对象时的工作量。(2)标记清除后会产生大量不连续的内存碎片。

标记-清除算法

2 标记-整理算法

标记的过程都是一样的,整理是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。

优点:1 经过整理之后,新对象的分配只需要通过指针碰撞便能完成(Pointer Bumping),相当简单。2 使用这种方法空闲区域的位置是始终可知的,也不会再有碎片的问题了。

缺点:GC暂停的时间会增长,因为你需要将所有的对象都拷贝到一个新的地方,还得更新它们的引用地址。

标记-整理算法

3 复制算法

该算法的提出是为了克服句柄的开销和解决堆碎片的垃圾回收,将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。在对象区与空闲区的切换过程中,程序暂停执行。
复制算法比较适合于新生代(短生存期的对象),在老年代(长生存期的对象)中,对象存活率比较高,如果执行较多的复制操作,效率将会变低,所以老年代一般会选用其他算法,如标记—整理算法。
优点:1 标记阶段和复制阶段可以同时进行。2 每次只对一块内存进行回收,运行高效。3 只需移动栈顶指针,按顺序分配内存即可,实现简单。4 内存回收时不会有内存碎片的出现(活动对象所占的内存空间之间没有空闲间隔)。
缺点:需要一块能容纳下所有存活对象的额外的内存空间。因此,可一次性分配的最大内存缩小了一半

复制算法

4 分代收集算法

对新生代使用复制算法,对老年代使用标记-清理或者标记-整理算法。