Java應用程序中的內存泄漏及內存管理

內存泄漏

Java平臺的一個突出的特性是自動內存管理。很多人把這種特性誤讀爲Java沒有內存泄露。然而,在我印象中,現代Java框架以及基於Java的平臺並非如此。特別是Android平臺,能舉出很多反例。爲了讓大家對Java平臺的內存泄露有一個初步的認識,我們先來看一個Java實現的棧:

class SimpleStack {
 
    private final Object[] objectPool = new Object[10];
    private int pointer = -1;
 
    public Object pop() {
        if(pointer < 0) {
            throw new IllegalStateException("no elements on stack");
        }
        return objectPool[pointer--];
    }
 
    public Object peek() {
        if(pointer < 0) {
            throw new IllegalStateException("no elements on stack");
        }
        return objectPool[pointer];
 
    }
 
    public void push(Object object) {
        if(pointer > 8) {
            throw new IllegalStateException("stack overflow");
        }
        objectPool[++pointer] = object;
    }
}

這個棧的實現基於一個對象數組,並維護了一個用於指向棧內當前可用單元的整型指針。上面的實現中,每次從棧頂彈出元素都會產生內存泄露。確切的說,即使不再使用棧頂元素,對象數組會繼續持有棧頂元素的引用(除非棧頂元素再次入棧,棧頂元素的引用會被完全相同的引用覆蓋)。因此,即便這個對象的其他引用都被釋放,Java虛擬機也不能回收這個對象。由於這種棧實現並不允許外界直接訪問其底層的對象池,因此除非有新元素入棧並被放置在棧內的同一個位置上,否則這個無法訪問的引用將阻止垃圾回收器回收該對象。


幸運的是,這個內存泄露很容易修復:

public Object pop() {
    if (pointer < 1) {
        throw new IllegalStateException("no elements on stack");
    }
    try {
        return objectPool[pointer];
    } finally {
        objectPool[pointer--] = null;
    }
}

當然,在日常的Java開發中一般不會去實現一個內存數據結構。因此,讓我們來看一個更常見的Java內存泄漏的例子。在Java開發中經常用到的觀察者模式就會引起內存泄露:

class Observed {
 
    public interface Observer {
        void update();
    }
 
    private Collection<Observer> observers = new HashSet<Observer>();
 
    void addListener(Observer observer) {
        observers.add(observer);
    }
 
    void removeListener(Observer observer) {
        observers.remove(observer);
    }
 
}

這次提供了一個直接刪除底層對象池引用的方法。基於這種實現,任何已註冊的Observer在使用後只要被正確註銷,就不會存在內存泄漏的風險。然而,假設這樣一個場景,框架的使用者在使用完Observer之後並沒有及時註銷。同理Observer將永遠不會被回收,因爲Observed一直保留着它的引用。更糟的是,沒有Observer引用,是無法從Observed對象池外部刪除Observer的,即無法回收未被及時註銷的Observer。

不過,有一種簡單的方法能夠修復這種潛在的內存泄露——弱引用。我個人認爲這是Java程序員都應該知道的特性。簡單地說,弱引用在功能上和普通的引用一樣,但它不會妨礙垃圾回收。因此JVM執行垃圾回收時,如果沒有發現強引用,那麼你就會發現弱引用會被置爲null。要使用弱引用,我們可以將上面的代碼改爲:

private Collection<Observer> observers = Collections.newSetFromMap(
        new WeakHashMap<Observer, Boolean>());

WeakHashMap是一個現成的弱引用Map,Map的鍵都是弱引用對象。使用WeakHashMap後,被觀察者將不會阻止JVM對Observer進行垃圾回收。然而,你必須在代碼註釋中強調這一點。因爲這個特性可能引起一些問題,比如使用者想要註冊一個常駐內存的Observer(例如日誌庫),但他們並沒有打算維持一個Observer引用。例如,Android平臺上的OnSharedPreferencesChangeListener使用了弱引用,但文檔中並沒有聲明這一特性。這給開發者帶來了很多麻煩。

在本文的開頭我提到了,現在的很多框架都需要使用者謹慎地管理內存。我想至少有兩個例子可以印證這個觀點。

Android平臺

Android應用程序的核心類採用了基於生命週期的編程模型。這意味着你不能自行創建和管理這些類的實例,這些實例將由Android操作系統在需要的時候替你創建(比如應用程序需要顯示某個特定的畫面)。同理,Android操作系統將會決定應用何時不再需要某個特定實例(比如用戶關閉了應用界面),並通過調用該實例特定的生命週期方法來通知該實例即將被刪除。但是,如果你將這個實例的引用泄露到某個全局上下文,Android JVM將不能對這個實例進行回收。這與Android本身的設計理念相違背。由於Android手機通常沒有限制應用程序的內存,即使在非常簡單的應用中,也會頻繁創建和銷燬對象,所以在清理引用時必須格外小心。

不幸的是,應用程序核心類引用很容易被泄露到外部。你能看出下面的例子是如何泄露引用的嗎?

class ExampleActivity extends Activity {
 
    @Override
    public void onCreate(Bundle bundle) {
        startService(new Intent(this, ExampleService.class).putExtra("mykey",
                new Serializable() {
                    public String getInfo() {
                        return "myinfo";
                    }
                }));
    }
}

如果你認爲是傳入Intent構造函數的this指針泄露了當前實例的引用,你就錯了。這個Intent對象僅用於啓動ExampleService,它會在ExampleService啓動之後被銷燬。然而,那個實現了Serializable接口的匿名內部類會持有閉包類ExampleActivity的引用。如果ExampleService一直維持着這個匿名類實例引用,那麼也會持有這個ExampleActivity實例的引用。


出於這個原因,我建議Android開發者避免使用匿名類。

Web應用框架(特別是Wicket)

Web應用框架通常將半永久性的用戶數據存放在Session中。你在Session中寫入的任何數據都會在內存中滯留,而且滯留的時間無法確定。如果有一定數量的訪問者在你的Session中“亂扔垃圾”,運行Servlet容器的JVM早晚會掛掉。因此,你謹慎管理引用的另一個極端案例就是Wicket框架:Wicket框架會將用戶的所有訪問序列化成歷史版本。這種過分簡單的設計意味着,如果某個訪問者點擊十次歡迎頁面,Wicket框架會在硬盤默認路徑下序列化十個對象。Wicket頁面對象持有的所有對象引用都會和頁面對象一起被序列化到硬盤上,所以在管理引用時必須格外小心。

讓我們來看一個錯誤使用Wicket框架的示例:
class ExampleWelcomePage extends WebPage {
 
    private final List<People> peopleList;
 
    public ExampleWelcomePage (PageParameters pageParameters) {
        peopleList = new Service().getWorldPhonebook();
    }
}

用戶點擊十次歡迎頁面,就會在服務器硬盤上存儲十份WorldPhoneBook拷貝。因此,在你使用Wicket開發應用時,務必要使用LoadableDetachableModels管理引用。

在Java程序中追蹤內存泄漏是一件非常麻煩的事情,因此我想推薦一款非常好用的(但很可惜不是免費的)調式工具:JProfiler。它能夠提供Java程序運行時的堆快照(heap dumps),幫助你瞭解程序運行時內部的具體情況。如果你的程序存在內存泄露的問題,我推薦你試一試JProfiler。JProfiler提供免費試用許可證。

原文鏈接: javacodegeeks 翻譯: ImportNew.com - 夏千林
譯文鏈接: http://www.importnew.com/8935.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章