Java應用程式中的記憶體洩漏及記憶體管理

testingbang發表於2019-08-29

近期發現測試的專案中有JAVA記憶體洩露的現象。雖然JAVA有垃圾回收的機制,但是如果不及時釋放引用就會發生記憶體洩露現象。在實際工作中我們使用Jprofiler呼叫java自帶的 jmap來做檢測還是很快能夠定位到錯誤。不過亡羊補牢不如先把羊圈修補得好一些。下面這篇文章給出了幾種常見的記憶體洩露型別。大家coding的時候注意一下。

btw,一些靜態程式碼掃描工具也能檢測出不好的程式設計習慣帶來潛在的記憶體洩露的風險。


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

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
classSimpleStack {
 
    privatefinalObject[] objectPool =newObject[10];
    privateintpointer = -1;
 
    publicObject pop() {
        if(pointer <0) {
            thrownewIllegalStateException("no elements on stack");
        }
        returnobjectPool[pointer--];
    }
 
    publicObject peek() {
        if(pointer <0) {
            thrownewIllegalStateException("no elements on stack");
        }
        returnobjectPool[pointer];
 
    }
 
    publicvoidpush(Object object) {
        if(pointer >8) {
            thrownewIllegalStateException("stack overflow");
        }
        objectPool[++pointer] = object;
    }
}

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

幸運的是,這個記憶體洩露很容易修復:

1
2
3
4
5
6
7
8
9
10
publicObject pop() {
    if(pointer <1) {
        thrownewIllegalStateException("no elements on stack");
    }
    try{
        returnobjectPool[pointer];
    }finally{
        objectPool[pointer--] =null;
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
classObserved {
 
    publicinterfaceObserver {
        voidupdate();
    }
 
    privateCollection<Observer> observers =newHashSet<Observer>();
 
    voidaddListener(Observer observer) {
        observers.add(observer);
    }
 
    voidremoveListener(Observer observer) {
        observers.remove(observer);
    }
 
}

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

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

1
2
privateCollection<Observer> observers = Collections.newSetFromMap(
        newWeakHashMap<Observer, Boolean>());

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

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

Android平臺

Android應用程式的核心類採用了基於生命週期的程式設計模型。這意味著你不能自行建立和管理這些類的例項,這些例項將由Android作業系統在需要的時候替你建立(比如應用程式需要顯示某個特定的畫面)。同理,Android作業系統將會決定應用何時不再需要某個特定例項(比如使用者關閉了應用介面),並透過呼叫該例項特定的生命週期方法來通知該例項即將被刪除。但是,如果你將這個例項的引用洩露到某個全域性上下文,Android JVM將不能對這個例項進行回收。這與Android本身的設計理念相違背。由於Android手機通常沒有限制應用程式的記憶體,即使在非常簡單的應用中,也會頻繁建立和銷燬物件,所以在清理引用時必須格外小心。

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

1
2
3
4
5
6
7
8
9
10
11
12
classExampleActivityextendsActivity {
 
    @Override
    publicvoidonCreate(Bundle bundle) {
        startService(newIntent(this, ExampleService.class).putExtra("mykey",
                newSerializable() {
                    publicString 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框架的示例:

1
2
3
4
5
6
7
8
classExampleWelcomePageextendsWebPage {
 
    privatefinalList<People> peopleList;
 
    publicExampleWelcomePage (PageParameters pageParameters) {
        peopleList =newService().getWorldPhonebook();
    }
}

使用者點選十次歡迎頁面,就會在伺服器硬碟上儲存十份WorldPhoneBook複製。因此,在你使用Wicket開發應用時,務必要使用LoadableDetachableModels管理引用。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69942496/viewspace-2655348/,如需轉載,請註明出處,否則將追究法律責任。

相關文章