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實現的。

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