您好,登錄后才能下訂單哦!
這篇文章主要介紹Java應(yīng)用程序中內(nèi)存泄漏及內(nèi)存管理的示例分析,文中介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們一定要看完!
btw,一些靜態(tài)代碼掃描工具也能檢測(cè)出不好的編程習(xí)慣帶來潛在的內(nèi)存泄露的風(fēng)險(xiǎn)。
Java平臺(tái)的一個(gè)突出的特性是自動(dòng)內(nèi)存管理。很多人把這種特性誤讀為Java沒有內(nèi)存泄露。然而,在我印象中,現(xiàn)代Java框架以及基于Java的平臺(tái)并非如此。特別是Android平臺(tái),能舉出很多反例。為了讓大家對(duì)Java平臺(tái)的內(nèi)存泄露有一個(gè)初步的認(rèn)識(shí),我們先來看一個(gè)Java實(shí)現(xiàn)的棧:
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; } } |
這個(gè)棧的實(shí)現(xiàn)基于一個(gè)對(duì)象數(shù)組,并維護(hù)了一個(gè)用于指向棧內(nèi)當(dāng)前可用單元的整型指針。上面的實(shí)現(xiàn)中,每次從棧頂彈出元素都會(huì)產(chǎn)生內(nèi)存泄露。確切的說,即使不再使用棧頂元素,對(duì)象數(shù)組會(huì)繼續(xù)持有棧頂元素的引用(除非棧頂元素再次入棧,棧頂元素的引用會(huì)被完全相同的引用覆蓋)。因此,即便這個(gè)對(duì)象的其他引用都被釋放,Java虛擬機(jī)也不能回收這個(gè)對(duì)象。由于這種棧實(shí)現(xiàn)并不允許外界直接訪問其底層的對(duì)象池,因此除非有新元素入棧并被放置在棧內(nèi)的同一個(gè)位置上,否則這個(gè)無法訪問的引用將阻止垃圾回收器回收該對(duì)象。
幸運(yùn)的是,這個(gè)內(nèi)存泄露很容易修復(fù):
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; } } |
當(dāng)然,在日常的Java開發(fā)中一般不會(huì)去實(shí)現(xiàn)一個(gè)內(nèi)存數(shù)據(jù)結(jié)構(gòu)。因此,讓我們來看一個(gè)更常見的Java內(nèi)存泄漏的例子。在Java開發(fā)中經(jīng)常用到的觀察者模式就會(huì)引起內(nèi)存泄露:
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); }
} |
這次提供了一個(gè)直接刪除底層對(duì)象池引用的方法。基于這種實(shí)現(xiàn),任何已注冊(cè)的Observer在使用后只要被正確注銷,就不會(huì)存在內(nèi)存泄漏的風(fēng)險(xiǎn)。然而,假設(shè)這樣一個(gè)場(chǎng)景,框架的使用者在使用完Observer之后并沒有及時(shí)注銷。同理Observer將永遠(yuǎn)不會(huì)被回收,因?yàn)镺bserved一直保留著它的引用。更糟的是,沒有Observer引用,是無法從Observed對(duì)象池外部刪除Observer的,即無法回收未被及時(shí)注銷的Observer。
不過,有一種簡(jiǎn)單的方法能夠修復(fù)這種潛在的內(nèi)存泄露——弱引用。我個(gè)人認(rèn)為這是Java程序員都應(yīng)該知道的特性。簡(jiǎn)單地說,弱引用在功能上和普通的引用一樣,但它不會(huì)妨礙垃圾回收。因此JVM執(zhí)行垃圾回收時(shí),如果沒有發(fā)現(xiàn)強(qiáng)引用,那么你就會(huì)發(fā)現(xiàn)弱引用會(huì)被置為null。要使用弱引用,我們可以將上面的代碼改為:
1 2 | privateCollection<Observer> observers = Collections.newSetFromMap( newWeakHashMap<Observer, Boolean>()); |
WeakHashMap是一個(gè)現(xiàn)成的弱引用Map,Map的鍵都是弱引用對(duì)象。使用WeakHashMap后,被觀察者將不會(huì)阻止JVM對(duì)Observer進(jìn)行垃圾回收。然而,你必須在代碼注釋中強(qiáng)調(diào)這一點(diǎn)。因?yàn)檫@個(gè)特性可能引起一些問題,比如使用者想要注冊(cè)一個(gè)常駐內(nèi)存的Observer(例如日志庫),但他們并沒有打算維持一個(gè)Observer引用。例如,Android平臺(tái)上的OnSharedPreferencesChangeListener使用了弱引用,但文檔中并沒有聲明這一特性。這給開發(fā)者帶來了很多麻煩。
在本文的開頭我提到了,現(xiàn)在的很多框架都需要使用者謹(jǐn)慎地管理內(nèi)存。我想至少有兩個(gè)例子可以印證這個(gè)觀點(diǎn)。
Android應(yīng)用程序的核心類采用了基于生命周期的編程模型。這意味著你不能自行創(chuàng)建和管理這些類的實(shí)例,這些實(shí)例將由Android操作系統(tǒng)在需要的時(shí)候替你創(chuàng)建(比如應(yīng)用程序需要顯示某個(gè)特定的畫面)。同理,Android操作系統(tǒng)將會(huì)決定應(yīng)用何時(shí)不再需要某個(gè)特定實(shí)例(比如用戶關(guān)閉了應(yīng)用界面),并通過調(diào)用該實(shí)例特定的生命周期方法來通知該實(shí)例即將被刪除。但是,如果你將這個(gè)實(shí)例的引用泄露到某個(gè)全局上下文,Android JVM將不能對(duì)這個(gè)實(shí)例進(jìn)行回收。這與Android本身的設(shè)計(jì)理念相違背。由于Android手機(jī)通常沒有限制應(yīng)用程序的內(nèi)存,即使在非常簡(jiǎn)單的應(yīng)用中,也會(huì)頻繁創(chuàng)建和銷毀對(duì)象,所以在清理引用時(shí)必須格外小心。
不幸的是,應(yīng)用程序核心類引用很容易被泄露到外部。你能看出下面的例子是如何泄露引用的嗎?
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"; } })); } } |
如果你認(rèn)為是傳入Intent構(gòu)造函數(shù)的this指針泄露了當(dāng)前實(shí)例的引用,你就錯(cuò)了。這個(gè)Intent對(duì)象僅用于啟動(dòng)ExampleService,它會(huì)在ExampleService啟動(dòng)之后被銷毀。然而,那個(gè)實(shí)現(xiàn)了Serializable接口的匿名內(nèi)部類會(huì)持有閉包類ExampleActivity的引用。如果ExampleService一直維持著這個(gè)匿名類實(shí)例引用,那么也會(huì)持有這個(gè)ExampleActivity實(shí)例的引用。
出于這個(gè)原因,我建議Android開發(fā)者避免使用匿名類。
Web應(yīng)用框架通常將半永久性的用戶數(shù)據(jù)存放在Session中。你在Session中寫入的任何數(shù)據(jù)都會(huì)在內(nèi)存中滯留,而且滯留的時(shí)間無法確定。如果有一定數(shù)量的訪問者在你的Session中“亂扔垃圾”,運(yùn)行Servlet容器的JVM早晚會(huì)掛掉。因此,你謹(jǐn)慎管理引用的另一個(gè)極端案例就是Wicket框架:Wicket框架會(huì)將用戶的所有訪問序列化成歷史版本。這種過分簡(jiǎn)單的設(shè)計(jì)意味著,如果某個(gè)訪問者點(diǎn)擊十次歡迎頁面,Wicket框架會(huì)在硬盤默認(rèn)路徑下序列化十個(gè)對(duì)象。Wicket頁面對(duì)象持有的所有對(duì)象引用都會(huì)和頁面對(duì)象一起被序列化到硬盤上,所以在管理引用時(shí)必須格外小心。
讓我們來看一個(gè)錯(cuò)誤使用Wicket框架的示例:
1 2 3 4 5 6 7 8 | classExampleWelcomePageextendsWebPage {
privatefinalList<People> peopleList;
publicExampleWelcomePage (PageParameters pageParameters) { peopleList =newService().getWorldPhonebook(); } } |
用戶點(diǎn)擊十次歡迎頁面,就會(huì)在服務(wù)器硬盤上存儲(chǔ)十份WorldPhoneBook拷貝。因此,在你使用Wicket開發(fā)應(yīng)用時(shí),務(wù)必要使用LoadableDetachableModels管理引用。
以上是“Java應(yīng)用程序中內(nèi)存泄漏及內(nèi)存管理的示例分析”這篇文章的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對(duì)大家有幫助,更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。