初學者或初級程序員在面試時如果能證明自己具有分析內存用量和內存調優的能力,這相當有利,因為這是針對5年左右相關經驗的高級程序員的要求。而對於高級程序員來說,如果能在面試時讓面試官感覺你確實做過內存調優的工作,那么面試官很有可能不問Java Core部分的其它問題了,畢竟虛擬機調優是Java Core部分非常資深的知識點。
在Java對象里,有強弱軟虛四種引用,它們都和垃圾回收流程密切相關,在項目里,我們可以通過合理地使用不同類型的引用來優化代碼的內存使用性能。
指向通過new得到的內存空間的引用叫強引用。比如有String a = newString(“123”);,其中的a就是一個強引用,它指向了一塊內容是123的堆空間。
平時我們用的最多的引用就是強引用,以至於很多人還不知道有其他類型引用的存在,下面我們來說下弱軟虛這三種平時不常見(但在關鍵時刻不可替代)的用途。
1 軟引用和弱引用的用法
軟引用(SoftReference)的含義是,如果一個對象只具有軟引用,而當前虛擬機堆內存空間足夠,那么垃圾回收器就不會回收它,反之就會回收這些軟引用指向的對象。
弱引用(WeakReference)與軟引用的區別在於,垃圾回收器一旦發現某塊內存上只有弱引用(一定請注意只有弱引用,沒強引用),不管當前內存空間是否足夠,那么都會回收這塊內存。
通過下面的ReferenceDemo.java,我們來看下軟引用和弱引用的用法,並對比一下它們的差別。
1 import java.lang.ref.SoftReference; 2 import java.lang.ref.WeakReference; 3 public class ReferenceDemo { 4 public static void main(String[] args) { 5 // 強引用 6 String str=new String("abc"); 7 SoftReference<String> softRef=new SoftReference<String>(str); // 軟引用 8 str = null; // 去掉強引用 9 System.gc(); // 垃圾回收器進行回收 10 System.out.println(softRef.get()); 11 // 強引用 12 String abc = new String("123"); 13 WeakReference<String> weakRef=new WeakReference<String>(abc); // 弱引用 14 abc = null; // 去掉強引用 15 System.gc(); // 垃圾回收器進行回收 16 System.out.println(weakRef.get()); 17 } 18 }
在第8行里,我們定義了SoftReference<String>類型的軟引用softRef,用來指向第7行通過new創建的空間,在第14行,我 們是通過弱引用weakRef指向第13行創建的空間。
接下來我們通過下表來觀察下具體針對內存空間的操作。
行號 |
針對內存的操作以及輸出結果 |
6 |
在堆空間里分配一塊空間(假設首地址是1000),在其中寫入String類型的abc,並用str這個強引用指向這塊空間。 |
7 |
用softRef這個軟引用指向1000號內存,這時1000號內存上有一個強引用str,一個軟引用softRef |
8 |
把1000號內存上的強引用str撤去,此時該塊內容上就只有一個軟引用softRef |
9 |
通過System.gc(),啟動垃圾回收動作 |
10 |
通過softRef.get()輸出軟引用所指向的值,此時1000號內存上沒有強引用,只有一個軟引用。但由於此時內存空間足夠,所以1000號內存上雖然只有一個軟引用,但第9行的垃圾回收代碼不會回收1000號的內存,所以這里輸出結果是123。 |
12 |
在堆空間里分配一塊空間(假設首地址是2000),在其中寫入String類型的123,並用abc這個強引用指向這塊空間。 |
13 |
用weakRef這個弱引用指向2000號內存,這時2000號內存上有一個強引用abc,一個軟引用weakRef |
14 |
把2000號內存上的強引用abc撤去,此時該塊內容上就只有一個弱引用weakRef |
15 |
通過System.gc(),啟動垃圾回收動作 |
16 |
通過weakRef.get()輸出軟引用所指向的值,此時2000號內存上沒有強引用,只有一個弱引用,所以第15行的垃圾回收代碼會回收2000號的內存,所以這里輸出結果是null。 |
2 軟引用的使用場景
比如在一個博客管理系統里,為了提升訪問性能,在用戶在點擊博文時,如果這篇博文沒有緩存到內存中,則需要做緩存動作,這樣其它用戶在點擊同樣這篇文章時,就能直接從內存里裝載,而不用走數據庫,這樣能降低響應時間。
我們可以通過數據庫級別的緩存在做到這點,這里也可以通過軟引用來實現,具體的實現步驟如下。
第一,可以通過定義Content類來封裝博文的內容,其中可以包括文章ID、文章內容、作者、發表時間和引用圖片等相關信息。
第二,可以定義一個類型為HashMap<String, SoftReference<Content>>的對象類保存緩存內容,其中鍵是String類型,表示文章ID,值是指向Content的軟引用。
第三,當用戶點擊某個ID的文章時,根據ID到第二步定義的HashMap里去找,如果找到,而且所對應的SoftReference<Content>值內容不是null,則直接從這里拿數據並做展示動作,這樣不用走數據庫,可以提升性能。
第四,如果用戶點擊的某個文章的ID在HashMap里找不到,或者雖然找到,但對應的值內容是空,那么就從數據庫去找,找到后顯示這個文章,同時再把它插入到HashMap里,這里請注意,顯示后需要撤銷掉這個Content類型對象上的強引用,保證它上面只有一個軟引用。
來分析下用軟引用有什么好處?假設我們用1個G的空間緩存了10000篇文章,這10000篇文章所占的內存空間上只有軟引用。如果內存空間足夠,那么我們可以通過緩存來提升性能,但萬一內存空間不夠,我們可以依次釋放這10000篇文章所占的1G內存,釋放后不會影響業務流程,最多就是降低些性能。
對比一下,如果我們這里不用軟應用,而是用強引用來緩存,由於不知道文章何時將被點擊,我們還無法得知什么時候可以撤銷這些文章對象上的強引用,或者即使我們引入了一套緩存淘汰流程,但這就是額外的工作了,這就沒剛才使用“軟引用“那樣方便了。
3 通過WeakHashMap來了解弱引用的使用場景
WeakHashMap和HashMap很相似,可以存儲鍵值對類型的對象,但我們可以從它的名字上看出,其中的引用是弱引用。通過下面的WeakHashMapDemo.java,我們來看下它的用法。
1 import java.util.HashMap; 2 import java.util.Iterator; 3 import java.util.Map; 4 import java.util.WeakHashMap; 5 public class WeakHashMapDemo { 6 public static void main(String[] args) throws Exception { 7 String a = new String("a"); 8 String b = new String("b"); 9 Map weakmap = new WeakHashMap(); 10 Map map = new HashMap(); 11 map.put(a, "aaa"); 12 map.put(b, "bbb"); 13 weakmap.put(a, "aaa"); 14 weakmap.put(b, "bbb"); 15 map.remove(a); 16 a=null; 17 b=null; 18 System.gc(); 19 Iterator i = map.entrySet().iterator(); 20 while (i.hasNext()) { 21 Map.Entry en = (Map.Entry)i.next(); System.out.println("map:"+en.getKey()+":"+en.getValue()); 22 } 23 Iterator j = weakmap.entrySet().iterator(); 24 while (j.hasNext()) { 25 Map.Entry en = (Map.Entry)j.next();System.out.println("weakmap:"+en.getKey()+":"+en.getValue()); 26 } 27 } 28 }
通過下表,我們來詳細說明關鍵代碼的含義。
行號 |
針對內存的操作以及輸出結果 |
7 |
在堆空間里分配一塊空間(假設首地址是1000),在其中寫入String類型的a,並用a這個強引用指向這塊空間。 |
8 |
在堆空間里分配一塊空間(假設首地址是2000),在其中寫入String類型的b,並用b這個強引用指向這塊空間。 |
11,12 |
在HashMap里了插入兩個鍵值對,其中鍵分別是a和b引用,這樣1000號和2000號內存上就分別多加了一個強引用了(有兩個強引用了)。 |
13,14 |
在WeakHashMap里了插入兩個鍵值對,其中鍵分別是a和b引用,這樣1000號和2000號內存上就分別多加了一個弱引用了(有兩個強引用,和一個弱引用)。 |
15 |
從HashMap里移出鍵是a引用的鍵值對,這時1000號內存上有一個String類型的強引用和一個弱引用。 |
16 |
撤銷掉1000號內存上的a這個強引用,此時1000號內存上只有一個弱引用了。 |
17 |
撤銷掉2000號內存上的b這個強引用,此時2000號內存上有一個HashMap指向的強引用和一個WeakHashMap指向的弱引用。 |
18 |
通過System.gc()回收內存 |
19~22 |
遍歷並打印HashMap里的對象,這里爭議不大,在11和12行放入了a和b這兩個強引用的鍵,在第15行移出a,所以會打印map:b:bbb。 |
23~25 |
遍歷並打印WeakHashMap里的對象,這里的輸出是weakmap:b:bbb。 雖然我們沒有從WeakHashMap里移除a這個引用,但之前a所對應的1000號內存上的強引用全都已經被移除,只有一個弱引用,所以在第18行時,1000號內存里的內存已經被回收,所以WeakHashMap里也看不到a了,只能看到b。 |
根據上文和這里的描述,我們知道如果當一個對象上只有弱引用時,這個對象會在下次垃圾回收時被回收,下面我們給出一個弱引用的使用場景。
比如在某個電商網站項目里,我們會用Coupan這個類來保存優惠券信息,在其中我們可以定義優惠券的打折程度,有效日期和所作用的商品范圍等信息。當我們從數據庫里得到所有的優惠券信息后,會用一個List<Coupan>類型的coupanList對象來存儲所有優惠券。
而且,我們想要用一種數據結構來保存一個優惠券對象以及它所關聯的所有用戶,這時我們可以用WeakHashMap<Coupan, <List<WeakReference <User>>>類型的weakCoupanHM對象。其中它的鍵是Coupan類型,值是指向List<User>用戶列表的弱引用。
大家可以想象下,如果有100個優惠券,那么它們會存儲於List<Coupan>類型的coupanList,同時,WeakHashMap<Coupan, <List<WeakReference <User>>>類型的weakCoupanHM對象會以鍵的形式存儲這100個優惠券。而且,如果有1萬個用戶,那么我們可以用List<User>類型的userList對象來保存它們,假設coupan1這張優惠券對應着100個用戶,那么我們一定會通過如下的代碼存入這種鍵值對關系,weakCoupanHM.put(coupan1,weakUserList);,其中weakUserList里以弱引用的方式保存coupan1所對應的100個用戶。
這樣的話,一旦當優惠券或用戶發生變更,它們的對應關系就能自動地更新,具體表現如下。
1 當某個優惠券(假設對應於coupan2對象)失效時,我們可以從coupanList里去除該對象,coupan2上就沒有強引用了,只有weakCoupanHM對該對象還有個弱引用,這樣coupan2對象能在下次垃圾回收時被回收,從而weakCoupanHM里就看不到了。
2 假設某個優惠券coupan3用弱引用的方式指向於100個用戶,當某個用戶(假設user1)注銷賬號時,它會被從List<User>類型的userList對象中被移除。這時該對象上只有weakCoupanHM里的值(也就是<List<WeakReference <User>>)這個弱引用,該對象同樣能在下次垃圾回收時被回收,這樣coupan3的關聯用戶就會自動地更新為99個。
如果不用弱引用,而是用常規的HashMap<Coupan,List<User>>來保存對應關系的話,那么一旦出現優惠券或用戶的變更的話,那么我們就不得不手動地更新這個表示對應關系的HashMap對象了,這樣,代碼就會變得復雜,而且我們很有可能因疏忽而忘記在某個位置添加更新代碼。相比之下,弱引用給我們帶來的“自動更新“就能給我們帶來很大的便利。
4 不能投機取巧,但面試確實有技巧
筆者寫本文的意思,不是讓大家投機取巧,事實上,如果大家只知道這些知識,而不知道其他虛擬機(或Java Core)相關的知識點,面試通過的可能性很低。
但話說回來,如果大家在平時開發時積累了很多經驗,但不會總結,在面試時也無法很好地展示各種能力,這樣也是非常可惜的。
根據本人在培訓學校的經驗,首先通過可能掌握各種Java技能,在這個基礎上再講述上述軟引用和弱引用的技能,這些候選人得到的反饋是,至少在Java Core方面比較精通。
在本文的其它博客里,也列了相關面試技巧,歡迎大家看其它的文章。