目录
上一篇日志简单总结了三种JVM垃圾回收方式,无论是使用引用计数、还是标记压缩清除法等,在进行垃圾回收时,都要先知道哪些对象是需要回收的,哪些是“活的”对象哪些是垃圾对象,判断这些对象的状态,方法是从根节点开始访问每一个对象,如果对象可达,表示该对象目前正在被使用,存在对其的引用,如果某一对象不可达,表示该已经不再被使用了,程序中不存在对其的引用,通常这类对象就是可以被回收的。但是有一种特殊情况就是对象的finalize()函数被调用,该函数只能被调用一次,调用后对象就有可能会复活。
finalize()函数复活对象
在某一对象不存在引用时,JVM垃圾回收会先判断这个对象是否覆盖了这个方法,如果没有覆盖,那么GC会直接将对象回收,如果对象覆盖了finalize()方法,那么会先执行finalize(),执行完毕后,GC再次判断该对象是否可达,存不存在引用,如果此时对象可达,那么对象就复活了,如果对象还是不可达,就可以进行回收,finalize()方法相当于给了对象一次“机会”。判断对象的状态,有三种结果,第一种是对象可触及,也就是说在GC回收对象前遍历过程中可以到达这个对象;第二种是不可触及,不可触及并不是简单的说对象在遍历后不可到达,而是说对象的finalize()函数被调用后,还是没有复活,此时才会进入不可触及状态,这时的对象确定没有任何引用了,可以进行回收。
示例
来看个具体的对象复活的例子:
public class ObjectRelive {
public static ObjectRelive myObject;
// 重写finalize()
@Override
protected void finalize() throws Throwable {
super.finalize();
myObject = this;
System.out.println("对象调用finalize()");
}
public static void main(String[] args) throws InterruptedException {
myObject = new ObjectRelive();
myObject = null;
System.out.println("第一次GC:");
System.gc();
Thread.sleep(2000);
if (myObject == null) {
System.out.println("myObject对象为null.");
} else {
System.out.println("myObject对象不为null.");
}
System.out.println("第二次GC:");
myObject = null;
System.gc();
Thread.sleep(2000);
if (myObject == null) {
System.out.println("myObject对象为null.");
} else {
System.out.println("myObject对象不为null.");
}
}
}
ObjectRelive类覆盖了finalize()方法,然后在主函数中,new一个ObjectRelive对象myObject,第14行将其置为null后,调用System.gc()进行第一次垃圾回收,此时虽然对象的引用被清楚了,但是类中的finalize()对象被覆盖,对象会调用该方法,执行里面的“myObject = this;”后,将当前对象的this传入方法里面,所以对象被复活了,变成了可触及态,按道理来说第一次GC之后myObject对象不为null。之后第26行开始我们进行第二次垃圾回收,还是先把myObject再次置为null,然后调用System.gc(),因为对象的finalize()方法只会被调用一次,所以对象无法再次复活,引用被清空后只能GC回收。
这里有些地方要注意的是,在我写代码时发现finalize()方法是一个已经被废弃的方法,查了些资料后说该方法在Java程序中并不能保证及时执行,通常在System.gc()后调用该方法,但也不能保证它一定会被执行。
引用强度
编写Java程序的我们虽然不用显式地去释放不需要的对象空间,但是如果我们想要“手动”去控制一些变量对象的生命周期,例如想要一个对象长期驻留的程序中,可以定义成强引用,如果想要JVM去自动管理回收某些对象,可以用弱引用或者软引用,例如对一些缓存数据,并不是特别重要,那么当内存空间不足时,就可以让GC回收这些缓存数据。除了强引用,弱引用和软引用外,还有一种叫虚引用,下面来简单看看这四种强度的引用。
不会被GC回收的强引用
强引用类型的对象在遍历过程中是可触及的,不会被GC回收,一个简单的例子:
String s1 = new String(“Reference”);
声明一个String类型的局部变量s1并实例化,这时变量s1会指向实例的堆空间(局部变量分配在栈空间上,实例分配在堆空间中),此时的变量s1对String实例就是强引用。强引用可以让变量s直接访问String实例,且因为强引用的存在,GC不会将它回收,好处是生命周期长,缺点是如果大量的强引用存在,如果内存空间不足,可能会造成内存泄漏。多个局部变量同时指向同一实例也是可以的,如果我们在声明一个String类型的变量s2,并让它也指向同一个实例:
String s1 = s2;
此时变量s2和s1指向的都是同一个实例,它们存放的地址是相同的,及时s1变量引用被释放掉,由于s2引用的存在,实例也不会被GC回收。
不可达后被GC回收的弱引用
弱引用类型对象在JVM进行垃圾回收时,只要被发现是不可达状态,就会被立刻回收,哪怕当前内存空间很充足,来看一个例子:
public class WeakRefInstance {
public static class Student {
public int studentID;
public String studentName;
public Student(int studentID, String studentName) {
this.studentID = studentID;
this.studentName = studentName;
}
@Override
public String toString() {
return "studnetID = " + studentID + ", studentName = " + studentName;
}
}
public static void main(String[] args) {
Student s = new Student(11, "张三");
WeakReference<Student> studentWeak = new WeakReference<Student>(s);
s = null;
System.out.println(studentWeak.get());
System.gc();
System.out.println(studentWeak.get());
}
}
在代码中,第19行首先声明了一个Student变量s并实例化,它是一个强引用,然后下一行构造了一个弱引用队列studentWeak,并获取对象s,之后显式将s置为null,这时Student实例失去了强引用。第22行我们从弱引用队列studentWeak中获取得到Student实例,从输出看是可以得到的,此时的Student实例就存在弱引用,接下来执行System.gc()进行垃圾回收,弱引用Student实例被发现不可达后,会立刻被回收掉:
从最后一句输出可以看到,它的确被GC回收,从弱引用队列中再次获取这个对象时得到的是null。