深入理解java中的Soft references amp;amp; Weak references amp;amp; Phantom reference
引言
Ethan Nicholas 在他的一篇文章中說:他面試了20多個Java高級工程師,他們每個人都至少有5年的Java從業經驗,當他問這些工程師對于Weak References 的理解時,只有其中的2個人知道Weak References 的存在,而這2個人中,只有1人知道如何去使用Weak References,而其他人甚至都不知道Java中有Weak References的存在。
大家可能會想,我接觸了Java這么多年,從來都沒有使用過Weak References啊,它真的是一個有用的技術嗎?對于它有用沒用,我相信大家看完這篇文章,理解了它以后,自有判斷。如果你從業了3年以上的Java開發,你不知道如何使用Weak References也還可以原諒,有可能你的項目還沒有那么復雜。但是如果你甚至都沒見過它在哪使用的,我覺得你可能讀的源碼太少了。我相信大家都知道ThreadLocal?這個類吧,你可以去看一下它的靜態內部類ThreadLocalMap, 如果你想見到更多它的應用,我給大家推薦個網站:searchcode,這個網站會根據你輸入的代碼片段,來搜索幾大源碼托管平臺上使用你輸入代碼片段的工程,大家可以輸入WeakReference試一試。
并不說只有你成為一個Weak References方面的專家,你才是一個優秀的Java開發者,但是,你至少要了解我們在什么樣的場景下需要使用它,That’s enough.
由于理解Weak References && soft references 會涉及到JVM的垃圾收集的一些知識,如果你對這方面沒有了解,請你參考我的這篇文章:Hotspot虛擬機- 垃圾收集算法和垃圾收集器
Java中的4種reference
在Java中,有4種reference類型,它們從強到弱,依次如下:
在這一小節中,我總結了各個引用的作用。如果大家不太明白,沒關系,我會在下文中更詳細地解釋它們各自的用法。
Strong reference
如果大家對垃圾收集機制有所了解,你們就會知道JVM標記一個對象是否為垃圾是根據可達性算法。 我們平常寫的代碼其實都是Strong reference,被Strong reference所引用的對象它會保持這個對象到GC roots的可達性,以防被JVM標記為垃圾對象,從而被回收。比如下面的代碼就是一個Strong reference
String str = new String("hello world");- 1
- 2
Soft reference
GC使Java程序員免除管理內存的痛苦,但是這并不意味著我們可以不關心對象的生命同期,如果我們不注意Java對象地生命周期,這很可能會導致Java出現內存泄露。
Object loitering
在我詳細解釋Soft reference之前,請大家先閱讀下面的這段代碼,仔細想一想它可能出現什么樣的問題?
public class LeakyChecksum {private byte[] byteArray;public synchronized int getFileChecksum(String fileName) {int len = getFileSize(fileName);if (byteArray == null || byteArray.length < len)byteArray = new byte[len];readFileContents(fileName, byteArray);// calculate checksum and return it} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
對于上面的程序而言,如果我把byteArray字節數組放到getFileChecksum方法中完全沒有問題,但是,上面的程序把byteArray字節數組從局部變量提升到實例變量會出現很多問題。比如,由于你需要共享byteArray變量,從而你不得不去考慮線程安全問題,而上面的程序在getFileChecksum方法上加上了synchronized?關鍵字,這大大降低了程序的可擴展性。
先不去深入討論上面程序出現的其它問題,讓我們來探討一下它出現的內存泄露問題。上述代碼的主要功能就是根據文件的內容去計算它的checksum,如果上述代碼的if?條件不成立,它會不斷地重用字節數組,而不是重新分配它。除非LeakyChecksum對象被gc,否則這個字節數組始終不會被gc,由于程序到它一直是可達的。而且更糟糕的是,隨著程序的不斷運行,這個字節數組只會不斷增大,不會減小,它的大小始終都和它處理過的最大的文件的大小一致,這樣很可能會導致JVM更頻繁地GC,降低應用程序地性能。大多數情況下,這個字節數組所占的空間要比它實際要用的空間要大,而多余的空間又不能被回收利用,這導致了內存泄露。
Soft references 解決上面的內存泄露問題
對于只被Soft references所引用的對象,我們稱它為softly reachable objects.?只要可得到的內存很充足,softly reachable objects 通常不會被gc. JVM要比我們的程序更加了解內存的使用情況,如果可得到的內存緊張,那么JVM就會頻繁地進行垃圾回收,從而釋放更多的內存空間,供我們使用。因此,上述程序的字節數組緩存由于一直是可達的,即使在內存很緊張的情況下,它也不會被回收掉,這無疑給垃圾收集器更大的壓力,使其更頻繁地GC.
那么有沒有一種解決方案可以做到這樣呢,如果我們的內存很充足,我們就保持這樣的緩存在內存中,不被gc; 但是,當我們的內存吃緊時,就把它釋放掉。那么大家想一想,誰可以做到這一點呢?答案是JVM,因為它最了解內存的使用情況,我們可以借助它的力量來達到我們的目標,而Soft references 可以幫我們Java 程序員借助JVM的力量。下面,讓我們來看看如果用SoftReference?改寫上面的代碼。
public class CachingChecksum {private SoftReference<byte[]> bufferRef;public synchronized int getFileChecksum(String fileName) {int len = getFileSize(fileName);byte[] byteArray = bufferRef.get();if (byteArray == null || byteArray.length < len) {byteArray = new byte[len];bufferRef.set(byteArray);}readFileContents(fileName, byteArray);// calculate checksum and return it} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
從上面的代碼我們可以看出,一旦走出if?語句,字節數組對象就只被Soft references 所引用,成為了softly reachable objects. 對于垃圾收集器來說,它只會在真正需要內存的時候才會去回收softly reachable objects. 現在,如果我們的內存不算吃緊,這個字節數組buffer會一直保存在內存中。在拋出OutOfMemoryError?之前,垃圾收集器一定會clear掉所有的soft references.
Soft references 與緩存
從上面的例子中,我們看到了如何用soft reference 去緩存1個對象,然后讓JVM去決定什么時候應該把對象從緩存中清除。對于嚴重依賴緩存提升性能的應用而言,用Soft references 做緩存并不合適,我們應該去找一個更全面的緩存框架去做這件事。但是由于它 “cheap and dirty” 的緩存機制, 對于一些小的應用場景,它還是很有吸引力的。
Weak References
由于JVM幫我們管理Java程序的內存,我們總是希望當一個對象不被使用時,它會被立即回收,即一個對象的邏輯生命周期要與它的實際生命周期相一致。但是有些時候,由于寫程序人的疏忽,沒有注意對象的生命周期,導致對象的實際生命周期要比我們期望它的生命周期要長。這種情況叫做?unintentional object retention.下面我們來看看由實例變量HashMap?導致的內存泄露問題。
HashMap 導致的內存泄露問題
用Map去關聯短暫對象的元數據很容易出現unintentional object retention?問題。比如你想關聯一個Socket連接與用戶的信息,由于你并不能干涉Socket?對象的實現,向里面加用戶數據,因此最常用的做法就是用全局Map來做這樣的事。代碼如下:
public class SocketManager {private Map<Socket,User> m = new HashMap<Socket,User>();public void setUser(Socket s, User u) {m.put(s, u);}public User getUser(Socket s) {return m.get(s);}public void removeUser(Socket s) {m.remove(s);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
通常情況下,Socket?對象的生命周期要比整個應用的生命周期要短,同時,它也會比用到它的方法調用要長。上述代碼把User?對象的生命周期與Socket?對象綁在一起,因為我們不能準確地知道Socket連接在什么時候被關閉,所以我們不能手動地去把它從Map中移除。而只要SocketManager?對象不死,HashMap?對象就始終是可達的。這樣就會出現一個問題,就是即使服務完來自客戶端的請求,Socket已經關閉,但是Socket?和?User?對象一直都不會被gc,它們會一直被保留在內存中。如果這樣一直下去,就會導致程序出現內存溢出的錯誤。
用 WeakHashMap 解決問題
既然上面的代碼有內存泄露的問題,我們應該如何解決呢?如果有一種手段可以做到,比如: 當Map中Entry的Key不再被使用了,就會把這個Entry自動移除,這樣我們就可以解決上面的問題了。幸運的是,Java團隊給我們提供了一個這樣的類可以做到這點,它就是WeakHashMap?,我們只要把我們的代碼做如下修改就行:
public class SocketManager {private Map<Socket,User> m = new WeakHashMap<Socket,User>();public void setUser(Socket s, User u) {m.put(s, u);}public User getUser(Socket s) {return m.get(s);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
哈哈,是不是很簡單,就把HashMap?替換成WeakHashMap?就行了。下面的內容引用自Java官方文檔?的說明:
Hash table based implementation of the Map interface, with weak keys. An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use. More precisely, the presence of a mapping for a given key will not prevent the key from being discarded by the garbage collector, that is, made finalizable, finalized, and then reclaimed. When a key has been discarded its entry is effectively removed from the map, so this class behaves somewhat differently from other Map implementations.
理解 Weak references
WeakHashMap?為什么會有這么神奇的功能,而我們的HashMap?卻沒有呢?下面是WeakHashMap?中的部分源碼:
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> {V value;final int hash;Entry<K,V> next;/*** Creates new entry.*/Entry(Object key, V value,ReferenceQueue<Object> queue,int hash, Entry<K,V> next) {super(key, queue);this.value = value;this.hash = hash;this.next = next;}}- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
大家可以看到,它的Entry?繼承了WeakReference?類,而我們的HashMap?卻沒有。再看它的構造函數,它的key被Weak references 所引用,但是它的value并沒有。接下來,我來解釋一下Weak references的作用。
一個只被Weak references所引用的對象,它被稱作weakly reachable object. 而這樣的對象不能阻止垃圾收集器對它的回收。 就像上面的源碼一樣,我們會在構造的時候,用Weak references去引用對象,如果被引用的對象沒有被gc,那么可以通過WeakReference?的get()?方法去獲取被引用的對象。但是,如果被引用的對象已經被垃圾回收或者有人調用了WeakReference.clear()?,那么get()?方法將始終返回null. 如果你想用get()?方法返回的結果,一個最佳的實踐就是你應該做一下非空檢查。總之,Weak reference并不會延長對象的生命周期。
現在我們回到上面那個Socket的問題上,當Socket?對象在其它地方被使用時,它不會被回收,而且我們依然可以用WeakHashMap?的?get()?方法去獲取它相關聯的數據,但是一旦Socket連接被關閉,即我們不再需要這個對象時,WeakHashMap?將不能阻止Socket?對象被回收,因此這完全達到了我們想要的結果。下面,讓我們來看看WeakHashMap?中get()?方法的源碼:
public V get(Object key) {Object k = maskNull(key); // 如果給定的key是null,則用NULL_KEYint h = hash(k); // 根據key算出它的hash值Entry<K,V>[] tab = getTable();int index = indexFor(h, tab.length); // 找到當前hash值所對應的bucket下標Entry<K,V> e = tab[index];while (e != null) { // 如果有hash沖突的情況下,會沿著鏈表找下去if (e.hash == h && eq(k, e.get()))return e.value;e = e.next;}return null; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
我已經把上面的代碼加了相應地注釋,相信大家理解起來會很容易。還有一點值得說的就是:由于給定的key已經被方法參數所引用,因此在get()?方法中,key并不會被垃圾回收。如果你想把WeakHashMap?變成線程安全的,你可以簡單地用Collections.synchronizedMap()?把它包裝一下就行。
Reference queues
大家現在可以看一看上面構造器的源碼,其中的一個參數對象是ReferenceQueue, 那么問題來了,這個東西是干什么用的呢?再具體說它的作用之前,讓我們來探討一下上面Weak references存在的問題。
上面我已經說過了,只有key是被Weak references所引用的,這樣就會出現一個問題,只要SocketManager?對象不被gc,那么WeakHashMap?對象就不會被gc,然后除非你手動地調用remove()?方法,不然它里面的Entry?也不會被gc,那么問題來了,即使你的key已經被gc了,但是key對應的value,整個Entry對象依然會被保留在內存中,如果一直這樣下去的話,就會導致內存溢出。
那么,我們如何解決上面出現的問題呢?按照一個正常人的思路來說,我覺得應該去周期性地掃描一下Map,根據get()?方法是否返回null來判斷當前Entry是否應該被清除。但是,如果Map中包含了大量的Entry,你這樣做會效率很低?,F在,Reference queues 大顯身手的時候到了。如果在你構造Weak references的時候,你給它關聯一個ReferenceQueue?對象,那么當一個Reference?對象被clear的時候,它會被放入給定的隊列當中。因此,你只需要從這個隊列中獲取Reference?對象,然后做相應地清理工作就行了。
WeakHashMap?中有個私有方法expungeStaleEntries,下面讓我們來看看它的源碼:
private void expungeStaleEntries() {for (Object x; (x = queue.poll()) != null; ) { // 遍歷引用隊列synchronized (queue) {@SuppressWarnings("unchecked")Entry<K,V> e = (Entry<K,V>) x; // Entry對象就是Weak referencesint i = indexFor(e.hash, table.length); // 根據hash值找到相應的bucket下標Entry<K,V> prev = table[i];Entry<K,V> p = prev;while (p != null) { // 在鏈表中找出給定key對應的Entry,然后清除指向它的引用Entry<K,V> next = p.next;if (p == e) {if (prev == e)table[i] = next;elseprev.next = next;// Must not null out e.next;// stale entries may be in use by a HashIteratore.value = null; // Help GCsize--;break;}prev = p;p = next;}}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
對于上面的代碼我已經給出了相應地注釋,主要就是while?循環中的算法你需要好好理解一下,其實也不難,它其實就是從鏈表中移除一個節點。其實大家可以去看一看WeakHashMap?的源碼,它的大部分操作都會調用這個私有方法,比如上面的get?方法中的getTable?方法。
至此,我已經結合WeakHashMap?的源碼,把Weak references的知識講完了,相信有了這個強大的武器,大家可以更自如地控制對象地可達性了。
Phantom References
Phantom References 與上面的幾個引用存在很大的不同,至少上面的Reference?對象通過它們的get()?方法可以獲取到它們所引用的對象,但是,PhantomReference?的只會返回null. 大家可能會想,既然這個引用連對象都取不到,那要它有什么用呢?如果你去看這個Reference?對象的源碼,你會發現只有PhantomReference?類的構造器必須指定一個ReferenceQueue?對象,而這就是重點,當然了,你也可以把它設置為null,但是那樣將沒有任何意義。因此,Phantom reference 的唯一作用就是它可以監測到對象的死亡,即,當你的對象真正從內存中移除時,指向這個對象的PhantomReference?就會被加入到隊列中。 下面是一個有助于你理解PhantomReference?的Demo:
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue;public class PhantomReferenceDemo {private final static ReferenceQueue<Object> queue = new ReferenceQueue<>();public static void main(String[] args) {Person p1 = new Person("小明");Person p2 = new Person("小花");Animal a1 = new Animal(p1, "dog");Animal a2 = new Animal(p2, "cat");p1 = null;p2 = null;Runtime.getRuntime().gc();waitMoment(2000); // 給gc點時間收集,有時gc收集速度很快,可以不用加這句代碼,我只不過是保險起見printReferenceQueue(queue);}static class Person {private String name;public Person(String name) {this.name = name;}public String getName() {return name;}}static class Animal extends PhantomReference<Object> {private String name;public Animal(Person referent, String name) {super(referent, queue);this.name = name;}public String getName() {return name;}}private static void waitMoment(long time) {try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}private static void printReferenceQueue(ReferenceQueue<Object> rq) {int size = 0;Object obj;while ( ( obj = rq.poll() ) != null ) {Animal a = (Animal) obj;System.out.println(a.getName());size++;}System.out.println("引用隊列大小為: " + size);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
一旦Person?對象被回收,那么指向它的Animal?對象就會被放進隊列中,大家可以把上面的12,13行代碼注釋掉,看看有什么不同的效果。還有一種場景有可能會用到Phantom reference,比如你已經把一張很大的圖片加載到內存當中,只有當你確定這張圖片從內存移除之后,我才會加載下一張圖片,這樣可以防止內存溢出錯誤。
用Phantom reference 驗證不靠譜的finalize 方法
在這一小節中,我讓大家來看看Java中的finalize方法有多“不靠譜”。假設我現在有1個重寫了finalize 方法的對象obj,它被收集的過程如下:
上面我只說了大致的過程,更詳細的請參考:When is the finalize() method called in Java?
下面來看我寫的一段Demo代碼:
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue;public class PhantomReferenceDemo2 {private final static ReferenceQueue<Object> queue = new ReferenceQueue<>();public static void main(String[] args) {Person p = new Person();PhantomReference<Person> pr = new PhantomReference<>(p, queue);p = null; // 使Person對象變的不可達// 這次gc會把Person對象標記為不可達的,由于它重寫了finalize,因此它會被放入到finalization隊列Runtime.getRuntime().gc();waitMoment(2000); // 給gc更多的時間去處理,并且去執行隊列中的finalize方法Runtime.getRuntime().gc(); // 再次發起gc,收集Person對象waitMoment(2000); // 給gc更多的時間去處理printReferenceQueue(queue); // 如果Person對象已經被回收,這個隊列中應該有值}static class Person {@Overrideprotected void finalize() throws Throwable {System.out.println("finalize method in Person");}}private static void waitMoment(long time) {try {Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();}}private static void printReferenceQueue(ReferenceQueue<Object> rq) {int size = 0;Object obj;while ( ( obj = rq.poll() ) != null ) {System.out.println(obj);size++;}System.out.println("引用隊列大小為: " + size);} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
上面的代碼我已經加了很詳細的注釋,這里我們用Phantom reference 去監控Person?對象的存活狀態。大家再看看下面的代碼:
import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue;public class PhantomReferenceDemo2 {private final static ReferenceQueue<Object> queue = new ReferenceQueue<>();public static void main(String[] args) {BlockFinalization bf = new BlockFinalization();PhantomReference<BlockFinalization> prbf = new PhantomReference<>(bf, queue);bf = null; // 使BlockFinalization對象變的不可達// 我讓BlockFinalization對象中的finalize方法睡了1000秒,這樣會導致主線程即使結束,finalize方法也不會執行完Runtime.getRuntime().gc();Person p = new Person();PhantomReference<Person> pr = new PhantomReference<>(p, queue);p = null; // 使Person對象變的不可達// 這次會把Person對象放入到finalization隊列Runtime.getRuntime().gc();waitMoment(2000);Runtime.getRuntime().gc();waitMoment(2000);// 如果這2個對象中的finalize方法不被執行完,它們都不會被回收,根據隊列輸出的值就可以看出來了printReferenceQueue(queue);}static class BlockFinalization {@Overrideprotected void finalize() throws Throwable {System.out.println("finalize method in BlockFinalization");Thread.sleep(1000000);}} }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
上面的代碼有時會很詭異,在我的Ubuntu系統上,有時會出現先執行Person?中的finalize?方法的可能,這樣就會導致Person?會被垃圾回收,如果是BlockFinalization?對象中的方法先被執行,那么2個對象都不會被回收。大家可以把第31行代碼注釋掉,看看效果。從上面的例子可以看到,finalize方法是非常不靠譜的,它不但有可能會導致對象無法回收,而且還有可能出現在程序的生命周期之內不被執行的可能。
所以建議大家千萬不要使用這個方法,如果你非要使用它,我建議你去看Effective Java中的Item 7: Avoid finalizers ,這本書中介紹了關于使用它的2個場景,以及一些技巧。
垃圾收集器如何對待 Reference 對象
如果你去看WeakReference,?SoftReference,?PhantomReference的源碼,你會發現它們都繼承了抽象的?Reference?類。那么現在我們來看看當垃圾收集器遇到?Reference?對象時,是如何對待它們的?
當垃圾收集器在追蹤heap,遇到Reference?對象時,它并不會標記Reference?對象所引用的對象。它會把遇到的Reference?對象放到一個隊列中,追蹤heap過后,它會標識出softly reachable objects。垃圾收集器會基于當前GC回收的內存大小和其它的一些原則,來決定soft references是否需要被clear.?大家可以看一看Reference?類的clear?方法。
如果垃圾收集器決定clear這些soft references ,并且這些soft references有相應地ReferenceQueue?,那么這些被clear 的Reference?對象會被放到ReferenceQueue?隊列中。注意:clear?Reference?對象并把它放入到隊列中是發生在被引用對象的finalization 或 garbage collection 實際發生之前。
如果垃圾收集器并不打算clear這些Reference?對象,那么它們對應地softly reachable objects會被當作GC roots,并用這些GC roots繼續追蹤heap,使得這些通過soft references可達的對象被標記。
處理完soft references 過后,接下來會找出weakly reachable objects. Weak references 會被直接clear掉,然后放到對應地?ReferenceQueue中。
所有的Reference?類型都會在放入ReferenceQueue?前被clear掉,因此后續的處理你將不可能訪問到Reference類型引用的對象。
由于要特殊地對待Reference?類型,因此在垃圾收集的過程中,無疑會增加額外的開銷。如果Reference 對象不被放到對應地ReferenceQueue?中,那么它本身也會被回收的,而且它可能會在它的引用對象回收之前被回收。
參考資料
Plugging memory leaks with soft references
Plugging memory leaks with weak references
Understanding Weak References Blog
What is the difference between a soft reference and a weak reference in Java?
When is the finalize() method called in Java?
Have you ever used Phantom reference in any project?
What is the difference between a soft reference and a weak reference in Java?
版權聲明:轉載請注明來源,謝謝 https://blog.csdn.net/xlinsist/article/details/57089288總結
以上是生活随笔為你收集整理的深入理解java中的Soft references amp;amp; Weak references amp;amp; Phantom reference的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java自定义注解实现前后台参数校验
- 下一篇: 推荐算法综述(一)