Java中的关于弱引用分析

        上一篇在研究动态代理的时候,遇到了WeakCache,研究了半天,才发现自己连java的强引用、弱引用都不了解,就决定先去研究这个东东,然后再去看WeakCache类。

        Java中存在四种引用,分别是强引用,软引用,弱引用和虚引用(也就是幽灵引用)。其中虚引用在查了很多资料后,还是不太懂它的原理以及使用场景,所以这一点在以后能顿悟了再写吧。

        先说强引用,这个很简单,也是Java中常用的,只要我们定义了一个变量指向一个实例,那就是一个强引用。如:

        String str=new String("test");

        User user=new User();

        str和user指向的就是强引用。当强引用指向了null后(user=null),jvm会判断该实例的GC root不可达,然后会回收这个对象。但是如果是这种情况:

    User user=new User();
    Company company=new Company(user);

        Company这个对象在构建过程中引用了user,当我们将user置为null后,jvm会判断到user实例的GC root仍然可达,因为他被company对象所依赖,在这种情况下,就会发生内存泄露问题。如果不好理解,再看另一个例子:

User zhangsan=new User("张三");
Map<User,Department> map=new HashMap<>();
map.put(zhangsan,department1);
zhangsan=null;

        当我们将zhangsan置空时,因为对应的这个User实例还在map中被引用,所以zhangsan不会被回收,如果我们这是一个本地缓存,这样一直处理下去,终有一天,会导致因内存耗尽而系统崩溃的。

        怎么解决这个问题呢?

        对于第一个例子,我们需要手动的将company也置为null。对于第二个例子,我们需要手动执行map.remove(zhangsan);

        其实这两个解决办法的目的,就是要人为的将我们想要回收的实例所有的依赖全部取消,好让jvm判定其确实可以被回收。但是这样依赖,无形中就会增加代码负担,并且很容易出问题,不知道什么时候该取消依赖。为了解决这个问题,就产生了软引用和弱引用的概念。

        软引用(SoftReference)和弱引用(WeakReference)几乎完全相同,唯一的区别就是软引用比弱引用又稍微“强壮”一些,即:当一个软引用被置为空时,jvm会对其进行标记,但不进行回收,直到内存耗尽时,才会对其进行回收。而弱引用则只要被置为空,jvm就会立即对齐进行回收。因为两者差不多,我就主要分析了一下弱引用,偶尔带这软引用做对比,所以以下就主要以弱引用来写了。

        弱引用其实是一个具有泛型的包装类,将传入的一个强引用转为了弱引用。包装以后,想要获取被包装的对象时,则需要使用get方法来获取。下面用几个测试代码来详细说明弱引用的使用。

        先看Test1,这个测试了软引用和弱引用的区别,注释已经写得很详细了:

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
/**
 * 
 * 演示SoftReference和WeakReference的区别
 * 关键在于SoftReference在gc时不会立刻回收,需要等到内存不足的时候才会回收
 * WeakReference在gc时会立刻回收
 * 
 * new String("11")和"11"的区别是一个是对象,一个是常量池,gc时会回收对象,但是不回收常量池
 * 
 * */
public class Test1 {
	public static void main(String[] args) throws InterruptedException {
		String value1 = new String("softReference1");
		SoftReference<String> softRefer = new SoftReference<>(value1 );
		System.out.println("before gc--soft:"+softRefer.get());//softReference1
		value1=null;
		System.gc();
		System.out.println("after gc--soft:"+softRefer.get());//softReference1
		
		String value2 = new String("weakReference1");
		WeakReference<String> weakRefer = new WeakReference<>(value2 );
		System.out.println("before gc--weak:"+weakRefer.get());//weakReference1
		value2=null;
		System.gc();
		System.out.println("after gc--weak:"+weakRefer.get());//null
		
		String value3 = "softReference2";
		SoftReference<String> softRefer2 = new SoftReference<>(value3 );
		System.out.println("before gc--soft:"+softRefer2.get());//softReference2
		value3=null;
		System.gc();
		System.out.println("after gc--soft:"+softRefer2.get());//softReference2
		
		String value4 = "weakReference2";
		WeakReference<String> weakRefer2 = new WeakReference<>(value4 );
		System.out.println("before gc--weak:"+weakRefer2.get());//weakReference2
		value4=null;
		System.gc();
		System.out.println("after gc--weak:"+weakRefer2.get());//weakReference2
	}
}

        Test1是进行了主动GC,那我们再来看下被动GC是如何进行的。为了更好地演示效果,需要先调整一下jvm参数,将初始空间改为1M,并打印GC日志:

    -XX:+PrintGCDetails
    -Xms1M
    -Xmx1M

public class Test2 {
	public static void main(String[] args) throws InterruptedException {
		String value = new String("reference");
//		SoftReference<String> refer = new SoftReference<>(value );
		WeakReference<String> refer = new WeakReference<>(value );
		
		int i=0;
		while(true) {
			System.out.println("第"+i+"次循环:"+refer.get());
			if(refer.get()==null) {
				break;
			}
			i++;
		}
		System.out.println(i);
	}
}

        这个代码是从网上找的例子,是因为当定义了value后,程序进入了一个死循环,一段时间后,jvm会发现value因为长时间没被引用,就会主动对其进行回收。当refer.get()结果为null时,说明该对象已经被回收了,然后退出死循环。分别执行软引用和弱引用代码,会看到软引用的死循环会一直进行下去,但是弱引用在执行一段时间后,会退出循环。这印证了上面所说的,弱引用会立刻回收,但是软引用直到内存不够用才会回收。

        并且在软引用执行中,会不断的看到有GC发生,但却没有将软引用回收。

        有意思的是,才测试上面这段代码时,自己曾写出一个bug来,也贴出来给大家看下。

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        String value = new String("reference");
        WeakReference<String> refer = new WeakReference<>(value );
		
        int i=0;
        while(true) {
            String s=refer.get();//不能用强引用s获取这个对象,并在下面使用,否则会导致弱引用变成了强引用
            System.out.println("第"+i+"次循环:"+refer.get());
            if(s==null) {//这步时关键,如果用refer.get()去判断,也不会出现死循环
                break;
            }
            i++;
        }
        System.out.println(i);
    }
}

        我最开始就是用常规的写法,用一个变量接收这个弱引用,然后再去判断,但此时却出现了死循环,也就意味着在这里虽然使用了弱引用,但是对象却没有被回收,这是为什么?

        因为一旦用s去接收弱引用,又回将这个弱引用编程了强引用,不过这样也无所谓,因为就算强引用长时间没被使用,也会被回收的,主要是下面最关键一步,用s去判断是否为空,这就是在使用强引用了,强引用一旦有被使用,那只要内存够用,他就永远不会被回收,所以就出现了死循环。

        有时我们想知道什么时候发生了弱引用回收,并针对回收的对象做一些处理,这就需要用到应用队列(ReferenceQueue)

        在弱引用定义时,可以传入一个ReferenceQueue用来接收被回收的引用。

public class Test3 {
	public static void main(String[] args) throws InterruptedException {
		
		String value = new String("reference");
		ReferenceQueue<String> queue=new ReferenceQueue<>();
		WeakReference<String> weakRefer = new WeakReference<>(value,queue);
		System.out.println("在GC之前调用queue.poll():"+queue.poll());//null
		System.out.println("在GC之前调用weakRefer.get():"+weakRefer.get());//reference
		value=null;
		System.out.println("value被置为null后调用queue.poll():"+queue.poll());//null
		System.out.println("value被置为null后weakRefer.get():"+weakRefer.get());//reference
		System.gc();
		Thread.sleep(1000);//GC后如果停顿一下,会看到queue中直接就会有数据
		System.out.println("发生GC后调用queue.poll() 第一次调用:"+queue.poll());//java.lang.ref.WeakReference@6d06d69c
		System.out.println("发生GC后调用weakRefer.get():"+weakRefer.get());//null
		System.out.println("发生GC后调用queue.poll() 第二次调用:"+queue.poll());//null
		//说明:16~23行代码和下面的循环不同时测试
		int i=0;
//		while(true) {
//			System.out.println("循环中调用queue.poll():"+queue.poll());//一直都是null
//			System.out.println("循环中调用weakRefer.get():"+weakRefer.get());//一直都是对象
//			if(weakRefer.get()==null) {
//				System.out.println("当发生GC时调用queue.poll():"+queue.poll());//对象
//				System.out.println("当发生GC时调用weakRefer.get():"+weakRefer.get());//null
//				break;
//			}
//			i++;
//		}
		System.out.println(i);
	}
}

        代码中,我把主动GC和被动GC放一起了,节省篇幅。其中那个sleep是为了保证能看到效果,如果没有的话,会发现queue中没有任何引用入队。从测试中可以看到,只有当gc发生时,弱引用才会入队。

        最后,我们再看一下如何WeakHashMap的效果:

public class Test4 {
	public static void main(String[] args) throws InterruptedException {

		Map<String, String> weakMap = new WeakHashMap<>();
		String str1 = new String("weakMap1");
		String str2 = new String("weakMap2");
		weakMap.put(str1, "test");
		weakMap.put(str2, "test");
		System.out.println(weakMap.size());//2
		str1 = null;
		System.gc();
		Thread.sleep(1);// 需要有一个时间间隔,让JVM进行回收,否则看不到效果
		System.out.println(weakMap.size());//1

	}
}

        我们看到,gc发生后,map中的字段也减少了一个。简单看了一下WeakHashMap的源码,它的实现大体上和JDK1.6的HashMap类似(1.8以后HashMap改为了红黑树),主要是entry对象有了重大改变。WeakHashMap的Entry在实现了Map.entry接口的基础上,还继承了WeakReference<Object>对象,让map的键成为了弱引用,从而实现了jvm自动回收的功能。

        弱引用的场景大多使用在缓存中,比如常用的EhCache的底层就是采用了WeakReference实现的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章