目录
接着上一篇,总结完强引用和弱引用的例子,强引用简单来说情况就是局部变量表中存在变量指向了堆空间中的对象实例,可以存在多个变量同时指向同一实例,如果用关系运算符“==”来判断这些对象得到的是true,因为它们都指向了相同的堆空间地址,此时对象实例因为存在强引用所以GC不会对其进行回收。弱引用在GC过程中,只要扫描个对象时发现存在弱引用对象,按道理来说会立刻对它进行回收,实际情况中因为JVM垃圾回收的线程优先级并不高,可能不一定做到迅速,立即回收。
非立即回收的软引用
接着来看下一个,软引用,软引用要比弱引用强一点,当一个对象只存在软引用时,GC不会立刻回收它,而是当JVM堆空间不足时才会去回收,使用场景是对于一些有用但又不是必须长期存在的数据,例如缓存数据,网站中的图片缓存等,这些缓存数据有利于加快系统的速度,但是如果内存空间不足时,可以将它们清理掉。
示例
来看一个简单的例子:
public class SoftRefInstance {
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, "张三");
SoftReference<Student> studentSoft = new SoftReference<Student>(s);
s = null;
System.out.println(studentSoft.get());
System.gc();
System.out.println(studentSoft.get());
byte[] data = new byte[1024*819*10];
System.gc();
System.out.println(studentSoft.get());
}
}
第18行声明一个Student类的强引用实例,下一行创建一个软引用队列让强引用s建立软引用,接着s置为null消除强引用。我在Default VM arguments里配置JVM内存大小参数-Xmx10m运行程序,第23行进行第一次GC后,由于当前JVM内存空间充足,所以软引用还没有被回收,从输出中可以看到我们依然能获得软引用实例。之后在28行,程序需要申请一块较大的空间,这么做是为了让GC发现内存空间使用紧张,需要对空间中的软引用也进行回收,下一行进行一次GC后,再次从软引用队列中输出,可以看到获取到的数据为null,证明GC的确因为内存空间不足问题把软引用回收了。
总的来说,决定软引用会不会被GC回收的关键是内存空间,如果空间充足,那么软引用可暂时不回收,当空间利用紧张时,GC就会对软引用进行回收,由此可见,使用软引用不会出现内存泄漏问题。
引用强度最弱的虚引用
虚引用是四种引用类型中最弱的一种,如果一个实例对象只有虚引用,那么它和没有任何引用一样,随时都会被GC回收,这样形同虚设的引用类型作用之一是用来标记,如果垃圾回收过程中在回收对象是发现其存在虚引用,那么GC在回收它之前会先把它放入到对于的引用队列中,程序通过判断引用队列中是否有添加虚引用,来判断对象是否将要被GC回收,也就是说虚引用标记该对象处于即将被GC回收状态。
示例
来看个具体例子:
public class PhantomRef {
public static PhantomRef ins;
static ReferenceQueue<PhantomRef> phantomRefQueue = null; // 引用队列
public static class TraceRefQueue extends Thread {
@Override
public void run() {
while (true) {
if(phantomRefQueue != null) {
// 虚引用队列
PhantomReference<PhantomRef> phantomQueue = null;
try {
phantomQueue = (PhantomReference<PhantomRef>)phantomRefQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(phantomQueue != null) {
System.out.println("对象已被GC回收.");
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
ins = this;
System.out.println("对象调用finalize()");
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new TraceRefQueue());
t1.setDaemon(true);
t1.start();
phantomRefQueue = new ReferenceQueue<PhantomRef>(); // 初始化引用队列
ins = new PhantomRef();
// 建立虚引用
PhantomReference<PhantomRef> phantomQueue = new PhantomReference<PhantomRef>(ins, phantomRefQueue);
System.out.println("第一次GC:");
ins = null;
System.gc();
Thread.sleep(2000);
if (ins == null) {
System.out.println("对象为null.");
} else {
System.out.println("对象不为null.");
}
System.out.println("第二次GC:");
ins = null;
System.gc();
Thread.sleep(2000);
if (ins == null) {
System.out.println("对象为null.");
} else {
System.out.println("对象不为null.");
}
return;
}
}
首先我们定义一个PhantomRef类实例ins,和这个实例对象对应的引用队列phantomRefQueue,记住这个对象和这个引用队列是“一块的”,接着第5到24行我们自定义一个TraceRefQueue类来追踪虚引用对象的状态,里面是一个线程不停检测虚引用对象对应的引用队列中是否存在实例,如果发现与对象匹配的引用队列中remove出对象,证明该虚引用对象即将被GC回收了,所以它才会被加载进来。前面说了,虚引用对象在被GC回收时会先将其放到对应的引用队列中,如果对象ins只存在虚引用,那么它在GC回收前就会被放到其对应的引用队列phantomRefQueue中,所以说这个线程就是用来检测引用队列中是否存在ins对象来判断该对象是否将被GC回收。
第26到31行是覆盖了finalize()方法,上一篇日志说过当对象不存在引用时,JVM垃圾回收会先判断这个对象是否覆盖了这个方法,如果没有覆盖,那么GC会直接将对象回收,如果对象覆盖了finalize()方法,那么会先执行finalize(),该方法只会被执行一次,执行后对象复活。
来到主函数,第42行开始初始化虚引用队列,为ins对象建立虚引用并指定其对应的引用队列是phantomRefQueue,之后如果ins对象被GC回收,在回收之前它会被放到phantomRefQueue队列中。接下来程序会进行两次GC,第一次GC时由于对象执行了finalize()方法,对象复活,所以GC并没有把ins对象回收,第二次回收因为finalize()方法只会被执行一次,此时对象无法复活,由于存在虚引用,ins对象被添加到phantomRefQueue队列中,我们设置的TraceRefQueue类中的追踪线程检测到情况,所以输出信息,告诉大家该虚引用对象即将被GC回收。