JVM之垃圾回收算法

在java虚拟机的的堆中存放着java世界里几乎所有的对象实例,垃圾收集器收集的就是那些已经没有用的对象,没有用的对象即不可能在被任何途径使用。在进行垃圾收集前就是要找到这些没有用的对象。

1.引用计数算法

引用计数就是给对象添加一个引用计数器,每有一个地方引用它,它对应的计数器就加1,一个对象的引用计数器为0,说明这个对象在任何时候都不能被用到。但这个算法有个缺陷,就是不能解决循环引用的问题,如对象A引用了对象B,对象B引用了对象A,但没有任何对象用到了A和B,它们的引用计数器为1,却不可能被用到,所以使用此算法有些没有用的对象不会被发现。

2.可达性分析算法

这个算法是通过一系列叫做“GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如图,虽然Object5、Object6、Object7之间有所联系,但是它们没有连接到GC Roots,所以它们是可回收的对象。

分析对象是否是可回收

 

在可达性分析中不可达的对象还有一次自救的过程,即覆盖了对象的finalize()方法,如果在finalize()方法中,将自己和引用链中任意对象联系起来,那么就可能逃过被清除的命运,为什么是可能,因为finalize()的执行会在一个优先级较低的线程中执行,如果这个方法还没有执行,就进行了GC操作,那么还没等自己和引用链上的对象联系起来,就被清除了,而且finalize()方法只会执行一次,如果一次自救成功,但之后又不在引用链上了,在进行标记时就不会在执行这个方法,只能等着被清除。

垃圾收集算法

1.标记-清除算法

算法分为两个阶段,即标记,和清除,标记就是上面说的,先把不可访问对象进行标记,在标记完成后,GC会对堆内存从头到尾的进行遍历,若当前对象为不可可访问对象,就将其所占内存进行回收,这个算法的不足在于

  1. 标记和清除的效率不高,
  2. 清除后会产生大量不连续的内存碎片,空间碎片太多时会导致在分配大对象时,因为没有足够的空间而提早触发GC操作,导致GC频繁,造成卡顿。

2.复制算法

将可用内存按容量分为大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。但也有不足

1.如果存活的对象较多,复制操作就会很多,效率变低

2.内存空间只使用了一半

现代的虚拟机新生代的垃圾收集器采用的就是这种算法,但不是将内存分成两部分,而是三部分,一个较大的Eden空间,两个较小的Survivor(又叫s0,s1或者from,to)空间,每次使用Eden和一个Survivor空间,进行GC时将存活的的对象放到另一个Survivor中,在Survivor中每经历一次GC,还存活的对象,在它的对象头的GC标识部分会加1,当达到15后,会从新生代移到老年代。虚拟机Eden和Survivor的默认比例是8:1,使用-XX:SurvivorRatio=8(默认值) 可以更改.

另外,当Survivor空间不够时,需要老年代的内存进行分配担保。

对于上图来说,当Eden空间满了后,JVM进行mirror GC,将Eden中和s0中存活的对象复制到s1中,如果存活的对象大小大于s1的空间,就需要老年代进行担保,即一部分对象发在老年代中。

 

3.标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点(可回收对象并不多),有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。