在堆中存放着几乎所有的java对象实例,在GC执行垃圾回收之前,首先要区分出那些对象存活,哪些对象死亡,只有被标记为死亡的对象,GC才会在垃圾回收时释放其所占用的内存空间,这个过程被称为垃圾标记阶段
在jvm中,当一个对象已经不再被任何存活的对象继续引用时,就可以被宣判死亡,判断对象存活一般有两种方式:
- 引用计数法
- 可达性算法
1.不被java采用的引用计数法
引用计数法(Reference Counting),其实现过程相对简单:对每个对象保存一个整形引用计算器属性,用于记录对象被引用的情况,举个例子:对一个对象A,只要有一个对象引用了A,则A的引用计数器加1,当引用失效,引用计数器减1,当A计数器为0,表示对象A不能再被引用,可进行回收
其优点就是实现简单,垃圾对象易于辨识,判定效率高,回收没有延迟,虽然它有这么多优点,但为什么没有被Java采用?,是因为它有许多缺点:
- 需要单独的字段存储计数器,增加了存储的空间开销
- 伴随着加法减法操作,带来了时间开销
- 无法处理循环引用的问题,这是它的一个致命缺陷,这样可能导致内存泄漏
那么什么是循环引用呢?看下面的图:
简单来说:对象p指向了一个链表对象,而链表对象内部的计数器rc被循环引用,当p指向null时,由于循环引用导致rc不能清零导致无法被回收,进而导致内存泄漏,虽然java不采用这种方式,但某些其他语言采用了,比如Python
,Python提供了解决循环引用导致内存泄漏的解决方案:
- 手动解除
- 使用弱引用
2.可达性算法
对于这种算法,有许多不同的称呼,比如根搜索算法,追踪式垃圾收集(Tracing Garbage Collection),是java,c#等语言采用的判定对象死亡的垃圾收集算法,该算法可以有效解决循环引用问题,防止内存泄漏,其基本思路是:
- 以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所链接的对象是否可达
- 使用可达性算法后,内存中的存活对象都会被根对象直接或间接连接着,搜索所走过的路径称为引用链(Reference Chain)
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象
- 在可达性分析算法中,只有能被根对象集合直接或间接链接的对象才是存活对象
在java中,GC Roots包括以下几类元素:
- 虚拟机栈中引用的对象(局部变量表中的引用指向堆中的对象),比如各个线程被调用的方法使用到的参数,局部变量等,这是最为常见的一种
- 方法区中类静态属性引用的对象,比如java类的引用类型静态变量
- 方法区中常量引用的对象,比如字符串常量池里的引用
- 本地方法栈引用的对象
- 所有被同步锁
synchronized
持有的对象 - jvm虚拟机内部的引用:基本数据类型对应的Class对象,一些常驻的异常,系统类加载器
除了这些固定的GC Roots集合以外,根据用户所选的垃圾收集器以及当前回收的内存区域不同,某些对象可临时性的加入GC Roots集合,比如:分代收集和局部回收
3.对象的finalization机制
Java语言提供了对象终止(finalization)机制来允许开发人员提供对象销毁前自定义处理逻辑,当垃圾回收器发现没有一个引用指向对象,在回收这个对象之前先会调用这个对象的finalize()
方法,这个方法是Object类中的方法:
protected void finalize() throws Throwable { }
该方法允许子类重写,用于在对象被回收时进行资源释放,比如关闭数据库链接等,该方法在GC进行垃圾回收时由gc线程调用
注意:永远不要主动调用某个对象的finalize()
方法,应该交给垃圾回收机制调用,因为在执行这个方法的时候可能导致对象复活,并且该方法执行的时间时没有保障的,它完全由GC线程决定,极端情况下若不发生GC,则该方法永远不会被调用,一个糟糕的finalize()
会严重影响GC性能,
正是由于finalize()
的存在,虚拟机中的对象一般处于三种状态,可触及的,可复活的,不可触及的
- 可触及的,从根节点开始可以到达这个对象
- 不可触及的,对象的
finalize()
被调用,并且没有复活,则进入不可触及状态,在这种状态下对象不可能复活,因为finalize()只能被调用一次 - 可复活的,对象所有引用都被释放,但对象在finalize()中可能复活
这三种状态是由于finalize()的存在而进行的区分,只有对象在不可触及状态下才能被回收,通过GC垃圾回收器提供的Finalizer
线程来处理,该线程优先级比较低
如果从所有根节点都无法访问到某个对象,说明对象已经不再使用了,一般来说此对象需要被回收,但事实上,对象也并非必须立刻进行回收,一个无法触及的对象可能在某一条件下‘复活’自己,在这种情况下再进行回收就显得不合理了
我们通过一个例子来说明这个方法:
public class Alive {
static Alive alive;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用重写的finalize方法");
alive = this;
}
public static void main(String[] args) {
try {
alive = new Alive();
alive = null;//使对象没有引用
System.gc();//触发回收
System.out.println("第一次gc");
Thread.sleep(2000);//等待Finalizer线程执行
if(alive == null){
System.out.println("对象已死");
}else{
System.out.println("对象存活");
}
System.out.println("第二次gc");
alive = null;
System.gc();//触发回收
Thread.sleep(2000);//等待Finalizer线程执行
if(alive == null){
System.out.println("对象已死");
}else{
System.out.println("对象存活");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
将finalize()
注释前后分别进行测试:
注释前:
第一次gc
对象已死
第二次gc
对象已死
注释后:
调用重写的finalize方法
第一次gc
对象存活
第二次gc
对象已死
对于以上结果我们可以做出如下分析: