看深入理解java虚拟机一书,关于垃圾回收机制回收对象依据的总结:
对象的回收:
垃圾回收机制回收对象肯定是回收那些已经死亡的对象,不被使用的对象。确定死亡对象的依据可以使用引用计数算法。就是给对象中添加一个引用计数器,当该对象被引用时,计数器就+1;该引用失效时,计数器-1。当任意时刻,计数器的值为0时,就认为该对象是不再可能被使用的。这种算法简单且高效,并且有一些使用这种方式来管理内存。微软公司的COM(Component ject Model)技术、使用ActionScript 3的FlashPlayer、Python语言和在游戏脚本领域被广泛应用的Squirrel等。但是主流的java虚拟机没有使用这种方法,因为这种方法很难解决对象之间的相互循环引用问题。比如:
OOMObject oom1=new OOMObject();
OOMObject oom2=new OOMObject();
oom1.obj=oom2;
oom2.obj=oom1;
oom1=null;
oom2=null;
System.gc();
oom1和oom2相互引用不能被回收。但是在java虚拟中,使用的是根搜索算法。打印出的gc日志可以看出对象被回收。
[GC (System.gc()) 1147K->752K(19456K), 0.0011820 secs]
[Full GC (System.gc()) 752K->627K(19456K), 0.0053661 secs]
根搜索算法基本思路:通过一系列的名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(图论说法即对象到GC Roots不可达),则证明此对象不可用。java中的GC Roots有以下几种:
1.栈帧本地变量表中引用的对象;
2.方法区中的类静态属性引用的对象;
3.方法区中的常量引用的对象;
4.本地方法栈中JNI(Native方法)引用的对象。
即便是根搜索算法中不可达的对象,也并非一定被回收,因为被回收的对象只要要进行两次标记过程。如果对象在根搜索算法中没有与GC Roots相连,他将会被第一次标记并进行筛选。如果对象被判定有必要执行finalize(),对象被放置在F-Queue队列,如果在队列中被引用,那么会将对象移除回收集合,如果还是没有被引用,可能会被二次标记并回收。
方法区的回收:
方法区的回收主要是废弃常量和无用的类。回收废弃常量时,只要保证这个字面量没有在任何地方被引用就会被回收。但回收类的条件很苛刻,要保证以下条件才可能被回收:
1.该类所有实例都已经被回收,堆内不存在任何该类的实体;
2.加载该类的ClassLoader已经被回收;
3.该类对应的java.lang.Class没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
垃圾收集算法:
标记-清除算法:标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。主要缺点:一是效率问题,标记和清除的效率都不高;而是空间问题,清除之后产生大量不连续的内存碎片,空间碎片太多导致程序需要大量连续空间时不得不提前触发另一次垃圾回收动作。(图片来源:深入理解java虚拟机)
复制算法:将内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块内存用完了,就将还活着的对象复制到另一块上面,然后把已经使用的内存空间一次性清理掉。算法代价是,内存缩小为原来的一半。(图片来源:深入理解java虚拟机)
标记-整理算法:标记出所有需要回收的对象,在标记完成后让所有存活对象向一端移动,然后清理掉边界以外的内存。
分代收集算法:根据对象存活周期的不同将内存划分为几块。一般把java堆分为新生代和老年代,根据特点采用适合的算法。在新生代中,每次垃圾收集会有大量对象死去,只有少量对象存活,就使用复制算法,只需复制少量的对象存活成本可完成收集。而老年代中因为对象存活率高,可以使用“清理”或“整理”算法来进行回收。