我们知道JVM的内存结构包括五大区域:程序计数器、虚拟机栈、本地方法栈、堆区、方法区。其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生、随线程而灭,因此这几个区域的内存分配和回收都具备确定性,就不需要过多考虑回收的问题,所以堆区和方法区是垃圾收集器要关注的部分。
垃圾收集器在对堆区和方法区进行回收前,首先要确定这些区域的对象哪些可以被回收,哪些暂时还不能回收,这里用到了判断对象是否存活的算法,引用计数算法和可达性分析算法。这里就不对这两种算法做过多讨论,但是有一点需要注意无论是引用计数算法还是可达性分析算法都是基于强引用而言的。
下面开始介绍垃圾收集算法:
标记-清除算法
最基础的收集算法是标记-清除算法(mark-sweep),如同它的名字,算法分为标记和清除两个阶段;首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说他基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而来的。它的不足主要有两个:一个是效率问题,标记和清除两个过程效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前出发另一次垃圾收集动作。
回收前状态如下:
为了解决效率问题,一种称为复制的收集短发出现了。它将内存按容量划分为大小相等的两块,每次只使用其中一块。当一块使用完了,就将还存活的对象复制到另外一块上面。然后再讲已使用过的内存空间一次清理掉。这样使得每次都是对半个区的内存进行回收,也不用考虑内存碎片的问题了。只是这种算的的代价是将内存缩小为原来的一半。代价未免高了点。复制算法
现在的商业虚拟机都使用此种算法来回收新生代,根据IBM研究表明,新生代中98%的对象是朝生夕死的,所以并不需要按照1:1来划分内存空间,而是将新生代划分为一块较大的Eden和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。
回收时,将Eden和Survivor中存活的对象一次性复制到另外的Survivor空间,并清理掉Eden和Survivor空间。这样,可以减少浪费至10%,当survivor空间不够时,需要老年代进行分配担保(Handle Promotion,如果另一个Survivor没有足够空间,则存放到老年代)。
标记-整理算法
复制算法在存货对象较多时需要执行大量的复制操作,这对老年代来说是不合适的,因此有人就提出了标记-整理算法,过程与标记-清除算法一样,但后续步骤不是对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉边界之外的内存。
回收前:
回收后:
分代收集算法
当前的商业虚拟机均使用分代收集算法,将对象按照存活周期分为几块,一般将Java堆分为新生代与老年代,从而可以根据不同年代的特点使用不同的收集算法。
如新生代中存在大量死去的对象,只有少量存活,使用复制算法;老年代中因存活对象数量多,没有额外空间对它进行分配担保,就必须使用标记-清除或 标记–整理算法。