日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Java 理論與實踐: 用弱引用堵住內(nèi)存泄漏

 zhuge 2006-03-27


級別: 中級

Brian Goetz, 首席顧問, Quiotix

2005 年 12 月 19 日

雖然用 Java? 語言編寫的程序在理論上是不會出現(xiàn)“內(nèi)存泄漏”的,但是有時對象在不再作為程序的邏輯狀態(tài)的一部分之后仍然不被垃圾收集。本月,負責保障應用程序健康的工程師 Brian Goetz 探討了無意識的對象保留的常見原因,并展示了如何用弱引用堵住泄漏。

要讓垃圾收集(GC)回收程序不再使用的對象,對象的邏輯 生命周期(應用程序使用它的時間)和對該對象擁有的引用的實際 生命周期必須是相同的。在大多數(shù)時候,好的軟件工程技術保證這是自動實現(xiàn)的,不用我們對對象生命周期問題花費過多心思。但是偶爾我們會創(chuàng)建一個引用,它在內(nèi)存中包含對象的時間比我們預期的要長得多,這種情況稱為無意識的對象保留(unintentional object retention)。

全局 Map 造成的內(nèi)存泄漏

無意識對象保留最常見的原因是使用 Map 將元數(shù)據(jù)與臨時對象(transient object)相關聯(lián)。假定一個對象具有中等生命周期,比分配它的那個方法調(diào)用的生命周期長,但是比應用程序的生命周期短,如客戶機的套接字連接。需要將一些元數(shù)據(jù)與這個套接字關聯(lián),如生成連接的用戶的標識。在創(chuàng)建 Socket 時是不知道這些信息的,并且不能將數(shù)據(jù)添加到 Socket 對象上,因為不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這些信息,如清單 1 中的 SocketManager 類所示:


清單 1. 使用一個全局 Map 將元數(shù)據(jù)關聯(lián)到一個對象

                                    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);
                                    }
                                    }
                                    SocketManager socketManager;
                                    ...
                                    socketManager.setUser(socket, user);
                                    

這種方法的問題是元數(shù)據(jù)的生命周期需要與套接字的生命周期掛鉤,但是除非準確地知道什么時候程序不再需要這個套接字,并記住從 Map 中刪除相應的映射,否則,Socket 和 User 對象將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 Socket 和 User 對象被垃圾收集,即使應用程序不會再使用它們。這些對象留下來不受控制,很容易造成程序在長時間運行后內(nèi)存爆滿。除了最簡單的情況,在幾乎所有情況下找出什么時候 Socket 不再被程序使用是一件很煩人和容易出錯的任務,需要人工對內(nèi)存進行管理。





回頁首


找出內(nèi)存泄漏

程序有內(nèi)存泄漏的第一個跡象通常是它拋出一個 OutOfMemoryError,或者因為頻繁的垃圾收集而表現(xiàn)出糟糕的性能。幸運的是,垃圾收集可以提供能夠用來診斷內(nèi)存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 選項調(diào)用 JVM,那么每次 GC 運行時在控制臺上或者日志文件中會打印出一個診斷信息,包括它所花費的時間、當前堆使用情況以及恢復了多少內(nèi)存。記錄 GC 使用情況并不具有干擾性,因此如果需要分析內(nèi)存問題或者調(diào)優(yōu)垃圾收集器,在生產(chǎn)環(huán)境中默認啟用 GC 日志是值得的。

有工具可以利用 GC 日志輸出并以圖形方式將它顯示出來,JTune 就是這樣的一種工具(請參閱 參考資料)。觀察 GC 之后堆大小的圖,可以看到程序內(nèi)存使用的趨勢。對于大多數(shù)程序來說,可以將內(nèi)存使用分為兩部分:baseline 使用和 current load 使用。對于服務器應用程序,baseline 使用就是應用程序在沒有任何負荷、但是已經(jīng)準備好接受請求時的內(nèi)存使用,current load 使用是在處理請求過程中使用的、但是在請求處理完成后會釋放的內(nèi)存。只要負荷大體上是恒定的,應用程序通常會很快達到一個穩(wěn)定的內(nèi)存使用水平。如果在應用程序已經(jīng)完成了其初始化并且負荷沒有增加的情況下,內(nèi)存使用持續(xù)增加,那么程序就可能在處理前面的請求時保留了生成的對象。

清單 2 展示了一個有內(nèi)存泄漏的程序。MapLeaker 在線程池中處理任務,并在一個 Map 中記錄每一項任務的狀態(tài)。不幸的是,在任務完成后它不會刪除那一項,因此狀態(tài)項和任務對象(以及它們的內(nèi)部狀態(tài))會不斷地積累。


清單 2. 具有基于 Map 的內(nèi)存泄漏的程序

                                    public class MapLeaker {
                                    public ExecutorService exec = Executors.newFixedThreadPool(5);
                                    public Map<Task, TaskStatus> taskStatus
                                    = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
                                    private Random random = new Random();
                                    private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };
                                    private class Task implements Runnable {
                                    private int[] numbers = new int[random.nextInt(200)];
                                    public void run() {
                                    int[] temp = new int[random.nextInt(10000)];
                                    taskStatus.put(this, TaskStatus.STARTED);
                                    doSomeWork();
                                    taskStatus.put(this, TaskStatus.FINISHED);
                                    }
                                    }
                                    public Task newTask() {
                                    Task t = new Task();
                                    taskStatus.put(t, TaskStatus.NOT_STARTED);
                                    exec.execute(t);
                                    return t;
                                    }
                                    }
                                    

圖 1 顯示 MapLeaker GC 之后應用程序堆大小隨著時間的變化圖。上升趨勢是存在內(nèi)存泄漏的警示信號。(在真實的應用程序中,坡度不會這么大,但是在收集了足夠長時間的 GC 數(shù)據(jù)后,上升趨勢通常會表現(xiàn)得很明顯。)


圖 1. 持續(xù)上升的內(nèi)存使用趨勢
 

確信有了內(nèi)存泄漏后,下一步就是找出哪種對象造成了這個問題。所有內(nèi)存分析器都可以生成按照對象類進行分解的堆快照。有一些很好的商業(yè)堆分析工具,但是找出內(nèi)存泄漏不一定要花錢買這些工具 —— 內(nèi)置的 hprof 工具也可完成這項工作。要使用 hprof 并讓它跟蹤內(nèi)存使用,需要以 -Xrunhprof:heap=sites 選項調(diào)用 JVM。

清單 3 顯示分解了應用程序內(nèi)存使用的 hprof 輸出的相關部分。(hprof 工具在應用程序退出時,或者用 kill -3 或在 Windows 中按 Ctrl+Break 時生成使用分解。)注意兩次快照相比,Map.EntryTask 和 int[] 對象有了顯著增加。

請參閱 清單 3。

清單 4 展示了 hprof 輸出的另一部分,給出了 Map.Entry 對象的分配點的調(diào)用堆棧信息。這個輸出告訴我們哪些調(diào)用鏈生成了 Map.Entry 對象,并帶有一些程序分析,找出內(nèi)存泄漏來源一般來說是相當容易的。


清單 4. HPROF 輸出,顯示 Map.Entry 對象的分配點

                                    TRACE 300446:
                                    java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
                                    java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
                                    java.util.HashMap.put(<Unknown Source>:Unknown line)
                                    java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
                                    com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
                                    com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)
                                    





回頁首


弱引用來救援了

SocketManager 的問題是 Socket-User 映射的生命周期應當與 Socket 的生命周期相匹配,但是語言沒有提供任何容易的方法實施這項規(guī)則。這使得程序不得不使用人工內(nèi)存管理的老技術。幸運的是,從 JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命周期依賴性的方法,這樣垃圾收集器就可以幫助我們防止這種內(nèi)存泄漏 —— 利用弱引用。

弱引用是對一個對象(稱為 referent)的引用的持有者。使用弱引用后,可以維持對 referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,如果對一個對象的引用只有弱引用,那么這個 referent 就會成為垃圾收集的候選對象,就像沒有任何剩余的引用一樣,而且所有剩余的弱引用都被清除。(只有弱引用的對象稱為弱可及(weakly reachable)。)

WeakReference 的 referent 是在構造時設置的,在沒有被清除之前,可以用 get() 獲取它的值。如果弱引用被清除了(不管是 referent 已經(jīng)被垃圾收集了,還是有人調(diào)用了 WeakReference.clear()),get() 會返回 null。相應地,在使用其結果之前,應當總是檢查 get() 是否返回一個非 null 值,因為 referent 最終總是會被垃圾收集的。

用一個普通的(強)引用拷貝一個對象引用時,限制 referent 的生命周期至少與被拷貝的引用的生命周期一樣長。如果不小心,那么它可能就與程序的生命周期一樣 —— 如果將一個對象放入一個全局集合中的話。另一方面,在創(chuàng)建對一個對象的弱引用時,完全沒有擴展 referent 的生命周期,只是在對象仍然存活的時候,保持另一種到達它的方法。

弱引用對于構造弱集合最有用,如那些在應用程序的其余部分使用對象期間存儲關于這些對象的元數(shù)據(jù)的集合 —— 這就是 SocketManager 類所要做的工作。因為這是弱引用最常見的用法,WeakHashMap 也被添加到 JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。如果在一個普通 HashMap 中用一個對象作為鍵,那么這個對象在映射從 Map 中刪除之前不能被回收,WeakHashMap 使您可以用一個對象作為 Map 鍵,同時不會阻止這個對象被垃圾收集。清單 5 給出了 WeakHashMap 的 get() 方法的一種可能實現(xiàn),它展示了弱引用的使用:


清單 5. WeakReference.get() 的一種可能實現(xiàn)

                                    public class WeakHashMap<K,V> implements Map<K,V> {
                                    private static class Entry<K,V> extends WeakReference<K>
                                    implements Map.Entry<K,V> {
                                    private V value;
                                    private final int hash;
                                    private Entry<K,V> next;
                                    ...
                                    }
                                    public V get(Object key) {
                                    int hash = getHash(key);
                                    Entry<K,V> e = getChain(hash);
                                    while (e != null) {
                                    K eKey= e.get();
                                    if (e.hash == hash && (key == eKey || key.equals(eKey)))
                                    return e.value;
                                    e = e.next;
                                    }
                                    return null;
                                    }
                                    

調(diào)用 WeakReference.get() 時,它返回一個對 referent 的強引用(如果它仍然存活的話),因此不需要擔心映射在 while 循環(huán)體中消失,因為強引用會防止它被垃圾收集。WeakHashMap 的實現(xiàn)展示了弱引用的一種常見用法 —— 一些內(nèi)部對象擴展 WeakReference。其原因在下面一節(jié)討論引用隊列時會得到解釋。

在向 WeakHashMap 中添加映射時,請記住映射可能會在以后“脫離”,因為鍵被垃圾收集了。在這種情況下,get() 返回 null,這使得測試 get() 的返回值是否為 null 變得比平時更重要了。

用 WeakHashMap 堵住泄漏

在 SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清單 6 所示。(如果 SocketManager 需要線程安全,那么可以用 Collections.synchronizedMap() 包裝 WeakHashMap)。當映射的生命周期必須與鍵的生命周期聯(lián)系在一起時,可以使用這種方法。不過,應當小心不濫用這種技術,大多數(shù)時候還是應當使用普通的 HashMap 作為 Map 的實現(xiàn)。


清單 6. 用 WeakHashMap 修復 SocketManager

                                    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);
                                    }
                                    }
                                    

引用隊列

WeakHashMap 用弱引用承載映射鍵,這使得應用程序不再使用鍵對象時它們可以被垃圾收集,get() 實現(xiàn)可以根據(jù) WeakReference.get() 是否返回 null 來區(qū)分死的映射和活的映射。但是這只是防止 Map 的內(nèi)存消耗在應用程序的生命周期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵對象被收集后從 Map 中刪除死項。否則,Map 會充滿對應于死鍵的項。雖然這對于應用程序是不可見的,但是它仍然會造成應用程序耗盡內(nèi)存,因為即使鍵被收集了,Map.Entry 和值對象也不會被收集。

可以通過周期性地掃描 Map,對每一個弱引用調(diào)用 get(),并在 get() 返回 null 時刪除那個映射而消除死映射。但是如果 Map 有許多活的項,那么這種方法的效率很低。如果有一種方法可以在弱引用的 referent 被垃圾收集時發(fā)出通知就好了,這就是引用隊列 的作用。

引用隊列是垃圾收集器向應用程序返回關于對象生命周期的信息的主要方法。弱引用有兩個構造函數(shù):一個只取 referent 作為參數(shù),另一個還取引用隊列作為參數(shù)。如果用關聯(lián)的引用隊列創(chuàng)建弱引用,在 referent 成為 GC 候選對象時,這個引用對象(不是 referent)就在引用清除后加入 到引用隊列中。之后,應用程序從引用隊列提取引用并了解到它的 referent 已被收集,因此可以進行相應的清理活動,如去掉已不在弱集合中的對象的項。(引用隊列提供了與 BlockingQueue 同樣的出列模式 —— polled、timed blocking 和 untimed blocking。)

WeakHashMap 有一個名為 expungeStaleEntries() 的私有方法,大多數(shù) Map 操作中會調(diào)用它,它去掉引用隊列中所有失效的引用,并刪除關聯(lián)的映射。清單 7 展示了 expungeStaleEntries() 的一種可能實現(xiàn)。用于存儲鍵-值映射的 Entry 類型擴展了 WeakReference,因此當 expungeStaleEntries() 要求下一個失效的弱引用時,它得到一個 Entry。用引用隊列代替定期掃描內(nèi)容的方法來清理 Map 更有效,因為清理過程不會觸及活的項,只有在有實際加入隊列的引用時它才工作。


清單 7. WeakHashMap.expungeStaleEntries() 的可能實現(xiàn)

                                        private void expungeStaleEntries() {
                                    Entry<K,V> e;
                                    while ( (e = (Entry<K,V>) queue.poll()) != null) {
                                    int hash = e.hash;
                                    Entry<K,V> prev = getChain(hash);
                                    Entry<K,V> cur = prev;
                                    while (cur != null) {
                                    Entry<K,V> next = cur.next;
                                    if (cur == e) {
                                    if (prev == e)
                                    setChain(hash, next);
                                    else
                                    prev.next = next;
                                    break;
                                    }
                                    prev = cur;
                                    cur = next;
                                    }
                                    }
                                    }
                                    





回頁首


結束語

弱引用和弱集合是對堆進行管理的強大工具,使得應用程序可以使用更復雜的可及性方案,而不只是由普通(強)引用所提供的“要么全部要么沒有”可及性。下個月,我們將分析與弱引用有關的軟引用,將分析在使用弱引用和軟引用時,垃圾收集器的行為。





回頁首


參考資料

學習

獲得產(chǎn)品和技術
  • JTune:免費 JTune 工具,可以使用 GC 日志并以圖形方式顯示堆大小、GC 持續(xù)時間和其他有用的內(nèi)存管理數(shù)據(jù)。


討論




回頁首


關于作者

Brian Goetz 成為專業(yè)軟件開發(fā)人員已經(jīng)超過 18 年了。他是 Quiotix 的首席顧問,該公司是位于加利福尼亞 Los Altos 的軟件開發(fā)和咨詢公司。他參加了幾個 JCP 專家組。Brian 的 Java Concurrency In Practice 一書將于 2005 年末由 Addison-Wesley 出版。請在業(yè)界流行的出版物上查閱 Brian 已發(fā)表的和即將發(fā)表的文章。

    本站是提供個人知識管理的網(wǎng)絡存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導購買等信息,謹防詐騙。如發(fā)現(xiàn)有害或侵權內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多