话不多说,干就完了。
在之前我们说到堆空间是对象实例存放的地方。程序会一致运行,对象也可能一直创建,但是堆的内存空间是有限的,那么如何保证在程序运行过程中,堆空间一直有足够的内存来创建新的对象呢? 垃圾回收 ,垃圾回收将已经不使用的对象进行回收,释放内存空间,以便在分配新的对象时有足够的内存空间来进行分配。
在对堆空间进行垃圾回收之前,首先就是要确定哪些对象还"存活",哪些对象已经"死去"(也就是不回再被任何途径所使用)。垃圾回收只会针对死去的对象。
引用计数器算法就是在对象中添加一个引用计数器,每当有一个地方使用到该对象时,计数器值就加1,而当引用失效的时候,该计数器值就减1,只要当计数器的值为0的时候,就表示该对象已经不再被使用。
引用计数器算法实现简单,而且效率高。
很难解决对象之间的相互循环引用问题。
由于其缺点,所以目前主流的虚拟机中都没有选用引用计数器算法来管理内存。那么什么是对象的相互循环引用呢?通过下面代码实例来进行说明
public class Dog { private Cat cat; // 省略get/set方法 } public class Cat { private Dog dog; // 省略get/set方法 } public static void main(String[] args){ Dog dog = new Dog(); Cat cat = new Cat(); // 对象的相互循环引用 cat.dog = dog; dog.cat = cat; dog = null; cat = null; } 复制代码
在上述代码中,虽然dog,cat被置为null,也就是不再使用了,但是dog和cat之间存在相互引用,所以虚拟机并不会回收这两个对象。
可达性算法的基本思想就是通过一系列被称为"GC Roots"的对象作为起始点,由这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个GC Roots对象没有任何引用链相连时,则证明该对象是不可用的。
在Java中,可作为GC Roots的对象主要包括了以下几种:
需要说明的是,即使是在可达性算法中,不可达的对象,并非就会被标记为"非死不可"的对象。对于一个对象的死亡宣告,至少要经历两次标记的过程。
l对象经过可达性算法分析后,发现没有与GC Roots项链的引用链时,它会被标记一次,同时会进行筛选,筛选的条件就是此对象有没有必要执行finalize方法。如果该对象 没有覆盖finalize方法 或者说 finalize方法已经被虚拟机执行过 。那么虚拟机会认为这两种情况没有必要执行finalize方法。
如果对象被判定为有必要执行finalize方法,那么虚拟机会将对象防止在一个F-Queue队列中。同时虚拟机会自动建立一个低优先级的Finalizer线程去执行它。需要注意的是,执行仅仅表示虚拟机会触发这个对象的finalize方法,但并不会保证等待这个对象的运行结束。
finalize方法是对象逃脱死亡命运的最后一次机会,GC会对F-Queue队列中的对象做第二次标记。对象在finalize方法中,如果重新建立与引用链上的任何一个对象关联,例如将自己(this关键字)赋值给某个类变量或者对象的成员变量。那么在第二次标记时,会被移出"即将回收"的集合。
最终流程如上图所示。需要注意的是,对于任何 一个对象的finalize方法,都只会被系统自动调用一次 ,如果对象已经调用过finalize方法之后,那么它的finalize方法就不会被再次执行。在实际开发过程中,应当避免调用对象的finalize方法。
常见的垃圾收集算法有四类: 标记-清除算法、标记-整理算法、复制算法、分代算法 。下面分别依次介绍每一种算法的思想。
标记-清除算法是垃圾收集算法中最基础的算法。在之前说如何判定一个对象是否存活的算法中,GC Roots会对对象进行标记,而标记清除算法,则是根据GC Roots的标记判断该对象可回收,如下图:
通过途中可以很明显的看到,标记清除算法会带来一个问题,那就是 内存碎片化 。在说到堆空间的新生代时,也提到过内存的碎片化,其后果就是会影响到程序的性能。
标记整理算法的标记过程同标记-清除算法一致,但是后续过程存在差异,标记-整理算法在标记后不是立马对可回收的对象进行回收,而是让存活的对象都向一端移动,然后清除可用边界以外的对象,释放内存空间。
在上图中可以明显的看到,与标记-清除算法不通的是,它并 不会产生内存碎片 ,而是通过整理,使当前可用对象都会保存在一段连续的内存上。
在说到堆空间的新生代时,有说到新生对象从Eden区到Survivor区的流转过程,而这个过程正好就是复制算法的实际体现。复制算法会在内存中划分出两块大小相等的区域(假设为A、B),每次只使用其中一块,当A区域满了的时候,会将存活的对象复制一份到B区域,同时清空A区域。只需要移动堆顶的指针,按照顺序分配,也就不用考虑内存碎片的问题了。
在目前主流的商业虚拟机中,基本上采用的都是分代算法,分代算法实质上就是 根据对象的存活周期不同划分不同的区域 。一般是把Java堆划分成新生代和老年代。而针对不同的代采取不同的收集算法。例如在新生代中,通常会采用复制算法,而在老年代中,因为对象的存活率较高,一般使用标记-整理或标记-清除算法。
本章中主要讲解了垃圾回收的相关的算法。两个方面,一是判定对象是否存活的算法,二是垃圾回收算法,总结如下图:
不怕路歹行不怕大雨淋,心上一字敢 面对我的梦,甘愿来作憨人。 --<憨人>