一篇搞懂,Java內(nèi)存泄漏
點(diǎn)擊上方藍(lán)色字體,選擇“標(biāo)星公眾號”
優(yōu)質(zhì)文章,第一時間送達(dá)
? 作者?|??Luckyp1ng
來源 |? urlify.cn/fIvuQr
Java中的內(nèi)存管理
要了解Java中的內(nèi)存泄漏,首先就得知道Java中的內(nèi)存是如何管理的。
在Java程序中,我們通常使用 new 為對象分配內(nèi)存,而這些內(nèi)存空間都在堆上。
Java判斷對象是否可以回收使用的而是可達(dá)性分析算法。
這個算法的基本思路就是通過一系列名為 "GC Roots" 的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的,下圖對象 object5, object6, object7 雖然有互相判斷,但它們到 GC Roots 是不可達(dá)的,所以它們將會判定為是可回收對象。
在 Java 語言中,可作為 GC Roots 對象的包括如下幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
本地方法棧(Native方法)中引用的對象
方法區(qū)中類靜態(tài)屬性引用的對象
方法區(qū)中常量引用的對象
什么是Java中的內(nèi)存泄漏
Java 中的內(nèi)存泄漏,廣義并通俗的說,就是:不再會被使用的對象的內(nèi)存不能被回收,就是內(nèi)存泄漏。
Java 中的內(nèi)存泄漏與 C++ 中的表現(xiàn)有所不同。
在 C++ 中,所有被分配了內(nèi)存的對象,不再使用之后,都必須程序員手動的去釋放他們。但是在 Java 中,我們不用自己釋放內(nèi)存,無用的內(nèi)存由 GC 自動清理,這也極大的簡化了我們的編程工作。但實際有時候一些不再會被使用的對象在 GC 看來不能被釋放就會造成內(nèi)存泄漏。
對象都是有生命周期的,有的長,有的短。如果長生命周期的對象持有短生命周期的引用,就很可能會出現(xiàn)內(nèi)存泄漏。例如:
public?class?Test?{
?Object?object;
?public?void?method()?{
??object?=?new?Object();
??//?...
?}
}
這里的 object 實例,其實我們期望它只作用于 method() 方法中,且其他地方也不會再用到它,但是當(dāng) method() 方法執(zhí)行完之后,object對象所分配的內(nèi)存不會馬上被認(rèn)為是可以被釋放的對象。只有在 Test 類創(chuàng)建的對象被釋放后才會被釋放。嚴(yán)格地說,這就是一種內(nèi)存泄漏。解決辦法就是將 object 作為 method() 方法中的局部變量。當(dāng)然也可以在使用完 object 之后 將其置為 null。
public?class?Test?{
?Object?object;
?public?void?method()?{
??object?=?new?Object();
??//?...
??object?=?null;
?}
}
這樣,之前 new Object() 分配的內(nèi)存就可以被 GC 回收。
Java中內(nèi)存泄漏的例子
靜態(tài)集合類
如HashMap、LinkedList等等。如果這些容器為靜態(tài)的,那么它們的生命周期與程序一致,則容器中的對象在程序結(jié)束之前將不能被釋放,從而造成內(nèi)存泄漏。簡單而言,長生命周期的對象持有短生命周期對象的引用,盡管短生命周期的對象不再使用,但是因為長生命周期對象持有它的引用而導(dǎo)致不能被回收。
static?Vector?v?=?new?Vector();?
for?(int?i?=?1;?i<100;?i++)?
{?
????Object?o?=?new?Object();?
????v.add(o);?
????o?=?null;?
}
在這個例子中,代碼棧中存在 Vector 對象的引用 v 和 Object 對象的引用 o 。在 For 循環(huán),我們不斷的生成新的對象,然后將其添加到 Vector 對象中,之后將 o 引用置空。問題是當(dāng) o 引用被置空后,如果發(fā)生 GC,我們創(chuàng)建的 Object 對象是否能夠被 GC 回收呢?答案是否定的。因為, GC 在跟蹤代碼棧中的引用時,會發(fā)現(xiàn) v 引用,而繼續(xù)往下跟蹤,就會發(fā)現(xiàn) v 引用指向的內(nèi)存空間中又存在指向 Object 對象的引用。也就是說盡管o 引用已經(jīng)被置空,但是 Object 對象仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。如果在此循環(huán)之后, Object 對象對程序已經(jīng)沒有任何作用,那么我們就認(rèn)為此 Java 程序發(fā)生了內(nèi)存泄漏。
各種連接,如數(shù)據(jù)庫連接、網(wǎng)絡(luò)連接和IO連接等
在對數(shù)據(jù)庫進(jìn)行操作的過程中,首先需要建立與數(shù)據(jù)庫的連接,當(dāng)不再使用時,需要調(diào)用close方法來釋放與數(shù)據(jù)庫的連接。只有連接被關(guān)閉后,垃圾回收器才會回收對應(yīng)的對象。否則,如果在訪問數(shù)據(jù)庫的過程中,對Connection、Statement或ResultSet不顯性地關(guān)閉,將會造成大量的對象無法被回收,從而引起內(nèi)存泄漏。
變量不合理的作用域
一般而言,一個變量的定義的作用范圍大于其使用范圍,很有可能會造成內(nèi)存泄漏。另一方面,如果沒有及時地把對象設(shè)置為null,很有可能導(dǎo)致內(nèi)存泄漏的發(fā)生。
public?class?UsingRandom?{
?private?String?msg;
?public?void?receiveMsg(){?
??readFromNet();//?從網(wǎng)絡(luò)中接受數(shù)據(jù)保存到msg中
??saveDB();//?把msg保存到數(shù)據(jù)庫中
?}
}
如上面這個偽代碼,通過 readFromNet() 方法把接受的消息保存在變量 msg 中,然后調(diào)用 saveDB() 方法把 msg 的內(nèi)容保存到數(shù)據(jù)庫中,此時 msg 已經(jīng)就沒用了,由于 msg 的生命周期與對象的生命周期相同,此時 msg 還不能回收,因此造成了內(nèi)存泄漏。
實際上這個 msg 變量可以放在 receiveMsg() 方法內(nèi)部,當(dāng)方法使用完,那么 msg 的生命周期也就結(jié)束,此時就可以回收了。還有一種方法,在使用完 msg 后,把 msg 設(shè)置為 null,這樣垃圾回收器也會回收 msg 的內(nèi)存空間。
內(nèi)部類持有外部類
如果一個外部類的實例對象的方法返回了一個內(nèi)部類的實例對象,這個內(nèi)部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由于內(nèi)部類持有外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內(nèi)存泄露。
改變哈希值
當(dāng)一個對象被存儲進(jìn) HashSet 集合中以后,就不能修改這個對象中的那些參與計算哈希值的字段了,否則,對象修改后的哈希值與最初存儲進(jìn) HashSet 集合中時的哈希值就不同了,在這種情況下,即使在 contains 方法使用該對象的當(dāng)前引用作為的參數(shù)去 HashSet 集合中檢索對象,也將返回找不到對象的結(jié)果,這也會導(dǎo)致無法從 HashSet 集合中單獨(dú)刪除當(dāng)前對象,造成內(nèi)存泄露。
public?static?void?main(String[]?args)?
{?
????Set?set?=?new?HashSet ();?
????Person?p1?=?new?Person("唐僧","pwd1",25);?
????Person?p2?=?new?Person("孫悟空","pwd2",26);?
????Person?p3?=?new?Person("豬八戒","pwd3",27);?
????set.add(p1);?
????set.add(p2);?
????set.add(p3);?
????System.out.println("總共有:"+set.size()+"?個元素!");?//結(jié)果:總共有:3 個元素!
????p3.setAge(2);?//修改p3的年齡,此時p3元素對應(yīng)的hashcode值發(fā)生改變?
????set.remove(p3);?//此時remove不掉,造成內(nèi)存泄漏
????set.add(p3);?//重新添加,居然添加成功?
????System.out.println("總共有:"+set.size()+"?個元素!");?//結(jié)果:總共有:4 個元素!
????for?(Person?person?:?set)?
????{?
????????System.out.println(person);?
????}?
}
單例對象在被初始化后將在JVM的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部對象的引用,那么這個外部對象將不能被jvm正常回收,導(dǎo)致內(nèi)存泄露
緩存泄漏
內(nèi)存泄漏的另一個常見來源是緩存,一旦你把對象引用放入到緩存中,他就很容易遺忘,對于這個問題,可以使用 WeakHashMap 代表緩存,此種 Map 的特點(diǎn)是,當(dāng)除了自身有對 key 的引用外,此 key 沒有其他引用那么此 map 會自動丟棄此值
監(jiān)聽器和回調(diào)
內(nèi)存泄漏第三個常見來源是監(jiān)聽器和其他回調(diào),如果客戶端在你實現(xiàn)的 API 中注冊回調(diào),卻沒有顯示的取消,那么就會積聚。需要確?;卣{(diào)立即被當(dāng)作垃圾回收的最佳方法是只保存他的弱引用,例如將他們保存成為 WeakHashMap 中的鍵。
內(nèi)存泄露解決的原則
1.盡量減少使用靜態(tài)變量,類的靜態(tài)變量的生命周期和類同步的。
2.聲明對象引用之前,明確內(nèi)存對象的有效作用域,盡量減小對象的作用域,將類的成員變量改寫為方法內(nèi)的局部變量;
3.減少長生命周期的對象持有短生命周期的引用;
4.使用StringBuilder和StringBuffer進(jìn)行字符串連接,Sting和StringBuilder以及StringBuffer等都可以代表字符串,其中String字符串代表的是不可變的字符串,后兩者表示可變的字符串。如果使用多個String對象進(jìn)行字符串連接運(yùn)算,在運(yùn)行時可能產(chǎn)生大量臨時字符串,這些字符串會保存在內(nèi)存中從而導(dǎo)致程序性能下降。
5.對于不需要使用的對象手動設(shè)置null值,不管GC何時會開始清理,我們都應(yīng)及時的將無用的對象標(biāo)記為可被清理的對象;
6.各種連接(數(shù)據(jù)庫連接,網(wǎng)絡(luò)連接,IO連接)操作,務(wù)必顯示調(diào)用close關(guān)閉。
粉絲福利:實戰(zhàn)springboot+CAS單點(diǎn)登錄系統(tǒng)視頻教程免費(fèi)領(lǐng)取
???
?長按上方微信二維碼?2 秒 即可獲取資料
感謝點(diǎn)贊支持下哈?

