- 堆里面放着Java世界中的所有的对象实例,垃圾收集堆在对堆进行回收前的第一件事情就是确定,这些对象,哪些还活着,哪些已经死了。("死去"即不可能再被任何途径使用的对象)
判断对象是否存活?
- 引用计数算法
- 这种算法虽然会占用一定的内存空间,但是它的原理简单,判定效率也高,大多数情况下都是一个不错的算法。
- 不过主流的Java虚拟机里面没有选用技术算法来管理内存。 这个算法有很多例外情况需要考虑,必须要配合大量额外处理才能进行工作
- 举一个例子
public class ReferenceCountingtGC {
public Object instance = null;
public static final int _1MB = 1024 * 1024;
//这个成员属性的唯一意义就是占点内存,以便在GC日志中看清楚是否回收过
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC(){
ReferenceCountingtGC objA = new ReferenceCountingtGC();
ReferenceCountingtGC objB = new ReferenceCountingtGC();
objA.instance = objB;
objB.instance = objA;
//假设在这发生GC,objA和objB能否被回收?
System.gc();
}
}
- 这两个对象已经不可能再被访问了,但是它们因为互相对方,导致它们的引用计数都不为零。 所以引用计数算法就无法回收它们。
2. 可达性分析算法
- 主流的商用程序语言Java、C#、Lisp的内存管理子系统,都是通过可达性分析算法来判定对象是否存活。
- 这个算法的基本思路就是通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链。
- 如果某个对象到GC Roots间没有任何引用链相连。用图论的话来说就是GC Roots到这个对象不可达时,说明对象不可能再被使用。
- 后文还会提到分代收集和局部回收。 如果只针对Java堆中某一块区域发起垃圾收集时,必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的。
- 所以某个区域里的对象 完全有可能被 位于堆中其他区域的对象 所引用,这个时候就需要将这些关联区域的对象也一并加入到GC Roots集合中去。
再谈引用
- 无论是通过引用计数算法 判断 对象的引用数量。 还是通过可达性分析算法判断对象是否引用链可达。
- 判定对象是否存活都和"引用"离不开关系。
生存还是死亡?
- 即使在可达性分析算法中判定为 不可达的对象,也不是非死不可的。
- 这个时候它们还处于"缓刑"阶段,要真正宣告一个对象死亡,最多会经历两次标记过程。
- 如果在进行可达性分析后发现 没有与GC Roots相连接的引用链,那它将会被第一次标记
- 随后进行一次筛选,筛选的条件就是此对象是否有必要执行finalize()方法。 如果对象没有覆盖finalize方法或者finalize方法已经被虚拟机调用过。 那么虚拟机将这两种情况视为没有必要执行。
- 如果这个对象被判定为确有必要执行finalize方法,那么该对象会被放置在一个F-Queue队列之中,并在稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer线程去执行它们的finalize方法。
- finalize方法是对象逃脱死亡的最后机会,收集器将会对F-Queue中的对象进行第二次小规模的标记。如果对象要拯救自己,那么需要重新与引用链上的任何一个对象建立关联即可。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes , i am still alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable{
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因为Finalizer方法优先级很低,暂停0.5s,以等待它。
Thread.sleep(500);
if( SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no , i am dead");
}
//第二次自救失败
SAVE_HOOK = null;
System.gc();
//因为Finalizer方法优先级很低、暂停0.5秒,以等待它
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
}else{
System.out.println("no , i am dead");
}
}
}
- 但是不建议在Java中使用finalize方法来拯救对象。 try-finally或使用其他方法都可以做的更好。
回收方法区
- *中提到过可以不要求虚拟机在方法区中实现垃圾收集,方法区垃圾收集的"性价比"通常也是比较低的。
- 在Java堆中,尤其是在新生代中,对常规应用进行一次垃圾回收通常都可以回收70%或者99%的内存空间。 但是方法区由于苛刻的判定条件,其区域垃圾收集的回收成果往往远低于此。