<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    精彩的人生

    好好工作,好好生活

    BlogJava 首頁(yè) 新隨筆 聯(lián)系 聚合 管理
      147 Posts :: 0 Stories :: 250 Comments :: 0 Trackbacks

    級(jí)別: 中級(jí)

    Brian Goetz , 首席顧問(wèn), Quiotix

    2005 年 12 月 19 日

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

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

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

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


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

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


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

    -----------------------------------------------------------------------------------

    找出內(nèi)存泄漏

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

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

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


    清單 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 之后應(yīng)用程序堆大小隨著時(shí)間的變化圖。上升趨勢(shì)是存在內(nèi)存泄漏的警示信號(hào)。(在真實(shí)的應(yīng)用程序中,坡度不會(huì)這么大,但是在收集了足夠長(zhǎng)時(shí)間的 GC 數(shù)據(jù)后,上升趨勢(shì)通常會(huì)表現(xiàn)得很明顯。)


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

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

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

    請(qǐng)參閱 清單 3

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


    清單 4. HPROF 輸出,顯示 Map.Entry 對(duì)象的分配點(diǎn)


    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)


    -------------------------------------------------------------------------------------------

    弱引用來(lái)救援了

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

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

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

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

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


    清單 5. WeakReference.get() 的一種可能實(shí)現(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() 時(shí),它返回一個(gè)對(duì) referent 的強(qiáng)引用(如果它仍然存活的話),因此不需要擔(dān)心映射在 while 循環(huán)體中消失,因?yàn)閺?qiáng)引用會(huì)防止它被垃圾收集。WeakHashMap 的實(shí)現(xiàn)展示了弱引用的一種常見(jiàn)用法 —— 一些內(nèi)部對(duì)象擴(kuò)展 WeakReference。其原因在下面一節(jié)討論引用隊(duì)列時(shí)會(huì)得到解釋。

    在向 WeakHashMap 中添加映射時(shí),請(qǐng)記住映射可能會(huì)在以后“脫離”,因?yàn)殒I被垃圾收集了。在這種情況下,get() 返回 null,這使得測(cè)試 get() 的返回值是否為 null 變得比平時(shí)更重要了。

    用 WeakHashMap 堵住泄漏

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


    清單 6. 用 WeakHashMap 修復(fù) 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);
        }
    }
    


    引用隊(duì)列

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

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

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

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


    清單 7. WeakHashMap.expungeStaleEntries() 的可能實(shí)現(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;
    ??????????? }
    ??????? }
    ??? }


    ------------------------------------------------------------------------------------------------

    結(jié)束語(yǔ)

    弱引用和弱集合是對(duì)堆進(jìn)行管理的強(qiáng)大工具,使得應(yīng)用程序可以使用更復(fù)雜的可及性方案,而不只是由普通(強(qiáng))引用所提供的“要么全部要么沒(méi)有”可及性。下個(gè)月,我們將分析與弱引用有關(guān)的軟引用,將分析在使用弱引用和軟引用時(shí),垃圾收集器的行為。

    -----------------------------------------------------------------------------------------------------


    原文地址:http://www-128.ibm.com/developerworks/cn/java/j-jtp11225/index.html

    posted on 2006-04-03 11:31 hopeshared 閱讀(596) 評(píng)論(0)  編輯  收藏 所屬分類(lèi): Java
    主站蜘蛛池模板: 免费a级黄色毛片| 亚洲精品乱码久久久久久V| 国产精品国产免费无码专区不卡| 免费国产成人午夜在线观看| 国产成人精品亚洲一区| 亚洲国产品综合人成综合网站| 亚洲人成人77777网站| 亚洲高清无码专区视频| 成人a免费α片在线视频网站| 亚洲免费中文字幕| 人妻免费一区二区三区最新| 日本特黄特色AAA大片免费| 亚洲欧美中文日韩视频| 亚洲fuli在线观看| 91亚洲国产成人精品下载| 亚洲av无码片在线播放| 久久亚洲av无码精品浪潮| 免费观看四虎精品国产永久 | 亚洲AV一宅男色影视| 亚洲精品NV久久久久久久久久| 成年女人毛片免费播放视频m| 色影音免费色资源| 久久久久久曰本AV免费免费| 七色永久性tv网站免费看| 青柠影视在线观看免费高清| 久久久精品视频免费观看 | 国产免费69成人精品视频| 日产乱码一卡二卡三免费| 成年性生交大片免费看| 大陆一级毛片免费视频观看i| 91在线视频免费看| 成年女人男人免费视频播放| 欧洲精品免费一区二区三区| 免费观看的毛片手机视频| 午夜免费福利影院| 日本免费观看网站| 亚洲精品综合久久| 国产亚洲精品岁国产微拍精品| 久久精品国产亚洲AV麻豆不卡 | 久久免费精彩视频| 222www免费视频|