前言:
Java語(yǔ)言的一個(gè)關(guān)鍵的優(yōu)勢(shì)就是它的內(nèi)存管理機(jī)制。你只管創(chuàng)建對(duì)象颂斜,Java的垃圾回收器幫你分配以及回收內(nèi)存夫壁。然而,實(shí)際的情況并沒有那么簡(jiǎn)單沃疮,因?yàn)閮?nèi)存泄漏在Java應(yīng)用程序中還是時(shí)有發(fā)生的盒让。
1. 什么是內(nèi)存泄漏?
????創(chuàng)建的對(duì)象不再被其他應(yīng)用程序使用司蔬,但因?yàn)楸黄渌麑?duì)象所引用著(即通過(guò)可達(dá)性分析糯彬,從GC Roots具有到該對(duì)象的鏈路),因此垃圾回收器沒辦法回收它們葱她。
2. 為什么會(huì)發(fā)生內(nèi)存泄漏撩扒?
????下面這個(gè)例子中,A對(duì)象引用B對(duì)象,A對(duì)象的生命周期(t1-t4)比B對(duì)象的生命周期(t2-t3)長(zhǎng)的多搓谆。當(dāng)B對(duì)象沒有被應(yīng)用程序使用之后炒辉,A對(duì)象仍然在引用著B對(duì)象。這樣泉手,垃圾回收器就沒辦法將B對(duì)象從內(nèi)存中移除黔寇,從而導(dǎo)致內(nèi)存問(wèn)題,因?yàn)槿绻鸄引用更多這樣的對(duì)象斩萌,那將有更多的未被引用對(duì)象存在缝裤,并消耗內(nèi)存空間。B對(duì)象也可能會(huì)持有許多其他的對(duì)象颊郎,那這些對(duì)象同樣也不會(huì)被垃圾回收器回收憋飞。所有這些沒在使用的對(duì)象將持續(xù)的消耗之前分配的內(nèi)存空間。
3. 常見發(fā)生內(nèi)存泄漏情況
????第二節(jié)說(shuō)到過(guò)姆吭,由于某些生命周期長(zhǎng)的對(duì)象引用了生命周期短的對(duì)象榛做,而此時(shí)生命周期短的對(duì)象并沒有被任何程序使用,依據(jù)jvm虛擬機(jī)規(guī)范内狸,這些沒有被任何程序使用的對(duì)象按理應(yīng)該被垃圾收集器進(jìn)行回收检眯,但由于存在引用,因此沒辦法進(jìn)行回收昆淡。最常見的情景便是靜態(tài)集合類锰瘸。
<1>靜態(tài)集合(全局集合)
????在使用Set、Vector昂灵、HashMap等集合類的時(shí)候需要特別注意避凝,有可能會(huì)發(fā)生內(nèi)存泄漏。當(dāng)這些集合被定義成靜態(tài)的時(shí)候倔既,由于它們的生命周期跟應(yīng)用程序一樣長(zhǎng)恕曲,此時(shí),若往靜態(tài)集合類中存放創(chuàng)建的java對(duì)象時(shí)渤涌,很可能發(fā)生內(nèi)存泄漏佩谣。示例代碼:
package com.lm.jvm;
import java.util.HashSet;
import java.util.Set;
/**
* @author lm
* @create 2018-10-12 21:28
* @desc java內(nèi)存泄漏1:靜態(tài)集合類
**/
public class MemoryLeak {
static Set<Object> set = new HashSet<>();
int size;
public void initSet(){
for (int i = 0; i < size; i++) {
Object o = new Object();
set.add(o);
o = null;
}
}
}
????如上圖代碼所示,循環(huán)創(chuàng)建了Object對(duì)象实蓬,并添加到靜態(tài)集合Set中茸俭,雖然將對(duì)象設(shè)置為null(不再使用),但由于靜態(tài)成員變量生命周期與類的生命周期一致安皱,即生命周期長(zhǎng)的對(duì)象引用著不再被任何程序使用的生命周期短的對(duì)象调鬓,因此這些本該要被回收的對(duì)象并不能被GC,因此造成了內(nèi)存泄漏酌伊。
<2>監(jiān)聽器
????在Java中腾窝,我們經(jīng)常會(huì)使用到監(jiān)聽器,如對(duì)某個(gè)控件添加單擊監(jiān)聽器addOnClickListener()
,但往往釋放對(duì)象的時(shí)候會(huì)忘記刪除監(jiān)聽器虹脯,這就有可能造成內(nèi)存泄漏驴娃。好的方法就是,在釋放對(duì)象的時(shí)候循集,應(yīng)該記住釋放所有監(jiān)聽器唇敞,這就能避免了因?yàn)楸O(jiān)聽器而導(dǎo)致的內(nèi)存泄漏。
<3>各種連接
????Java中的連接包括數(shù)據(jù)庫(kù)連接咒彤、網(wǎng)絡(luò)連接和IO
連接疆柔,如果沒有顯式調(diào)用其close()
方法,是不會(huì)自動(dòng)關(guān)閉的镶柱,這些連接就不能被GC回收而導(dǎo)致內(nèi)存泄漏旷档。一般情況下,在try
代碼塊里創(chuàng)建連接奸例,在finally
里釋放連接彬犯,就能夠避免此類內(nèi)存泄漏向楼。
<4>外部模塊的引用
????調(diào)用外部模塊的時(shí)候查吊,也應(yīng)該注意防止內(nèi)存泄漏。如模塊A調(diào)用了外部模塊B的一個(gè)方法湖蜕,如:
public void register(Object o)
這個(gè)方法有可能就使得A模塊持有傳入對(duì)象的引用逻卖,這時(shí)候需要查看B模塊是否提供了去除引用的方法,如unregister()
昭抒。這種情況容易忽略评也,而且發(fā)生了內(nèi)存泄漏的話,比較難察覺灭返,應(yīng)該在編寫代碼過(guò)程中就應(yīng)該注意此類問(wèn)題盗迟。
<5> 單例模式
????使用單例模式的時(shí)候也有可能導(dǎo)致內(nèi)存泄漏。因?yàn)閱卫龑?duì)象初始化后將在JVM的整個(gè)生命周期內(nèi)存在熙含,如果它持有一個(gè)外部對(duì)象(生命周期比較短)的引用罚缕,那么這個(gè)外部對(duì)象就不能被回收,而導(dǎo)致內(nèi)存泄漏怎静。如果這個(gè)外部對(duì)象還持有其它對(duì)象的引用邮弹,那么內(nèi)存泄漏會(huì)更嚴(yán)重,因此需要特別注意此類情況蚓聘。這種情況就需要考慮下單例模式的設(shè)計(jì)會(huì)不會(huì)有問(wèn)題腌乡,應(yīng)該怎樣保證不會(huì)產(chǎn)生內(nèi)存泄漏問(wèn)題。
<6>緩存
????緩存一種用來(lái)快速查找已經(jīng)執(zhí)行過(guò)的操作結(jié)果的數(shù)據(jù)結(jié)構(gòu)夜牡。因此与纽,如果一個(gè)操作執(zhí)行需要比較多的資源并會(huì)多次被使用,通常做法是把常用的輸入數(shù)據(jù)的操作結(jié)果進(jìn)行緩存,以便在下次調(diào)用該操作時(shí)使用緩存的數(shù)據(jù)急迂。緩存通常都是以動(dòng)態(tài)方式實(shí)現(xiàn)的,如果緩存設(shè)置不正確而大量使用緩存的話則會(huì)出現(xiàn)內(nèi)存溢出的后果硝岗,因此需要將所使用的內(nèi)存容量與檢索數(shù)據(jù)的速度加以平衡。
????常用的解決途徑是使用java.lang.ref.SoftReference類堅(jiān)持將對(duì)象放入緩存袋毙。這個(gè)方法可以保證當(dāng)虛擬機(jī)用完內(nèi)存或者需要更多堆的時(shí)候型檀,可以釋放這些對(duì)象的引用。
<7> 類裝載器
????Java類裝載器的使用為內(nèi)存泄漏提供了許多可乘之機(jī)听盖。一般來(lái)說(shuō)類裝載器都具有復(fù)雜結(jié)構(gòu)胀溺,因?yàn)轭愌b載器不僅僅是只與"常規(guī)"對(duì)象引用有關(guān),同時(shí)也和對(duì)象內(nèi)部的引用有關(guān)皆看。比如數(shù)據(jù)變量仓坞,方法和各種類。這意味著只要存在對(duì)數(shù)據(jù)變量腰吟,方法无埃,各種類和對(duì)象的類裝載器,那么類裝載器將駐留在JVM中毛雇。既然類裝載器可以同很多的類關(guān)聯(lián)嫉称,同時(shí)也可以和靜態(tài)數(shù)據(jù)變量關(guān)聯(lián),那么相當(dāng)多的內(nèi)存就可能發(fā)生泄漏灵疮。