1 引言
Java的一個(gè)重要優(yōu)點(diǎn)就是通過(guò)垃圾收集器GC (Garbage Collection)自動(dòng)管理內(nèi)存的回收,程序員不需要通過(guò)調(diào)用函數(shù)來(lái)釋放內(nèi)存。因此,很多程序員認(rèn)為Java 不存在內(nèi)存泄漏問(wèn)題,或者認(rèn)為即使有內(nèi)存泄漏也不是程序的責(zé)任,而是GC 或JVM的問(wèn)題。其實(shí),這種想法是不正確的,因?yàn)镴ava 也存在內(nèi)存泄漏,但它的表現(xiàn)與C++不同。如果正在開發(fā)的Java 代碼要全天24 小時(shí)在服務(wù)器上運(yùn)行,則內(nèi)存漏洞在此處的影響就比在配置實(shí)用程序中的影響要大得多,即使最小的漏洞也會(huì)導(dǎo)致JVM耗盡全部可用內(nèi)存。另外,在很多嵌入式系統(tǒng)中,內(nèi)存的總量非常有限。在相反的情況下,即便程序的生存期較短,如果存在分配大量臨時(shí)對(duì)象(或者若干吞噬大量?jī)?nèi)存的對(duì)象)的任何Java 代碼,而且當(dāng)不再需要這些對(duì)象時(shí)也沒有取消對(duì)它們的引用,則仍然可能達(dá)到內(nèi)存極限。
2 Java 內(nèi)存回收機(jī)制
Java 的內(nèi)存管理就是對(duì)象的分配和釋放問(wèn)題。分配內(nèi)存的方式多種多樣,取決于該種語(yǔ)言的語(yǔ)法結(jié)構(gòu)。但不論是哪一種語(yǔ)言的內(nèi)存分配方式,最后都要返回所分配的內(nèi)存塊的起始地址,即返回一個(gè)指針到內(nèi)存塊的首地址。在Java 中所有對(duì)象都是在堆(Heap)中分配的,對(duì)象的創(chuàng)建通常都是采用new或者是反射的方式,但對(duì)象釋放卻有直接的手段,所以對(duì)象的回收都是由Java虛擬機(jī)通過(guò)垃圾收集器去完成的。這種收支兩條線的方法確實(shí)簡(jiǎn)化了程序員的工作,但同時(shí)也加重了JVM的工作,這也是Java 程序運(yùn)行速度較慢的原因之一。因?yàn)?,GC 為了能夠正確釋放對(duì)象,GC 必須監(jiān)控每一個(gè)對(duì)象的運(yùn)行狀態(tài),包括對(duì)象的申請(qǐng)、引用、被引用、賦值等,GC 都需要進(jìn)行監(jiān)控。監(jiān)視對(duì)象狀態(tài)是為了更加準(zhǔn)確地、及時(shí)地釋放對(duì)象,而釋放對(duì)象的根本原則就是該對(duì)象不再被引用。Java 使用有向圖的方式進(jìn)行內(nèi)存管理,可以消除引用循環(huán)的問(wèn)題,例如有三個(gè)對(duì)象,相互引用,只要它們和根進(jìn)程不可達(dá),那么GC 也是可以回收它們的。在Java 語(yǔ)言中,判斷一塊內(nèi)存空間是否符合垃圾收集器收集標(biāo)準(zhǔn)的標(biāo)準(zhǔn)只有兩個(gè):一個(gè)是給對(duì)象賦予了空值null,以下再?zèng)]有調(diào)用過(guò),另一個(gè)是給對(duì)象賦予了新值,即重新分配了內(nèi)存空間。
3 Java 中的內(nèi)存泄漏
3.1 Java 中內(nèi)存泄漏與C++的區(qū)別 在Java 中,內(nèi)存泄漏就是存在一些被分配的對(duì)象,這些對(duì)象有下面兩個(gè)特點(diǎn),首先,這些對(duì)象是可達(dá)的,即在有向圖中,存在通路可以與其相連;其次,這些對(duì)象是無(wú)用的,即程序以后不會(huì)再使用這些對(duì)象。如果對(duì)象滿足這兩個(gè)條件,這些對(duì)象就可以判定為Java 中的內(nèi)存泄漏,這些對(duì)象不會(huì)被GC 所回收,然而它卻占用內(nèi)存。在C++中,內(nèi)存泄漏的范圍更大一些。有些對(duì)象被分配了內(nèi)存空間,然后卻不可達(dá),由于C++中沒有GC,這些內(nèi)存將永遠(yuǎn)收不回來(lái)。在Java 中,這些不可達(dá)的對(duì)象都由GC 負(fù)責(zé)回收,因此程序員不需要考慮這部分的內(nèi)存泄漏。通過(guò)分析,可以得知,對(duì)于C++,程序員需要自己管理邊和頂點(diǎn),而對(duì)于Java 程序員只需要管理邊就可以了(不需要管理頂點(diǎn)的釋放)。通過(guò)這種方式,Java 提高了編程的效率。
3.2 內(nèi)存泄漏示例
來(lái)源:(http://blog.sina.com.cn/s/blog_5d2b239b0100c6gm.html) - java內(nèi)存泄露_newboy_新浪博客 3.2.1 示例1 在這個(gè)例子中,循環(huán)申請(qǐng)Object 對(duì)象,并將所申請(qǐng)的對(duì)象放入一個(gè)Vector 中,如果僅僅釋放引用本身,那么Vector 仍然引用該對(duì)象,所以這個(gè)對(duì)象對(duì)GC 來(lái)說(shuō)是不可回收的。因此,如果對(duì)象加入到Vector 后,還必須從Vector 中刪除,最簡(jiǎn)單的方法就是將Vector對(duì)象設(shè)置為null。 Vector v = new Vector(10); for (int i = 1; i<100; i++) {Object o = new Object(); v.add(o); o = null; }// 此時(shí),所有的Object 對(duì)象都沒有被釋放,因?yàn)樽兞縱 引用這些對(duì)象。實(shí)際上無(wú)用,而還被引用的對(duì)象,GC 就無(wú)能為力了(事實(shí)上GC 認(rèn)為它還有用),這一點(diǎn)是導(dǎo)致內(nèi)存泄漏最重要的原因。 (1)如果要釋放對(duì)象,就必須使其的引用記數(shù)為0,只有那些不再被引用的對(duì)象才能被釋放,這個(gè)原理很簡(jiǎn)單,但是很重要,是導(dǎo)致內(nèi)存泄漏的基本原因,也是解決內(nèi)存泄漏方法的宗旨; (2)程序員無(wú)須管理對(duì)象空間具體的分配和釋放過(guò)程,但必須要關(guān)注被釋放對(duì)象的引用記數(shù)是否為0; (3)一個(gè)對(duì)象可能被其他對(duì)象引用的過(guò)程的幾種: a.直接賦值,如上例中的A.a = E; b.通過(guò)參數(shù)傳遞,例如public void addObject(Object E); c.其它一些情況如系統(tǒng)調(diào)用等。 3.3 容易引起內(nèi)存泄漏的幾大原因 3.3.1 靜態(tài)集合類像HashMap、Vector 等靜態(tài)集合類的使用最容易引起內(nèi)存泄漏,因?yàn)檫@些靜態(tài)變量的生命周期與應(yīng)用程序一致,如示例1,如果該Vector 是靜態(tài)的,那么它將一直存在,而其中所有的Object對(duì)象也不能被釋放,因?yàn)樗鼈円矊⒁恢北辉揤ector 引用著。
3.3.2 監(jiān)聽器在java 編程中,我們都需要和監(jiān)聽器打交道,通常一個(gè)應(yīng)用當(dāng)中會(huì)用到很多監(jiān)聽器,我們會(huì)調(diào)用一個(gè)控件的諸如addXXXListener()等方法來(lái)增加監(jiān)聽器,但往往在釋放對(duì)象的時(shí)候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機(jī)會(huì)。
3.3.3 物理連接 一些物理連接,比如數(shù)據(jù)庫(kù)連接和網(wǎng)絡(luò)連接,除非其顯式的關(guān)閉了連接,否則是不會(huì)自動(dòng)被GC 回收的。Java 數(shù)據(jù)庫(kù)連接一般用DataSource.getConnection()來(lái)創(chuàng)建,當(dāng)不再使用時(shí)必須用Close()方法來(lái)釋放,因?yàn)檫@些連接是獨(dú)立于JVM的。對(duì)于Resultset 和Statement 對(duì)象可以不進(jìn)行顯式回收,但Connection 一定要顯式回收,因?yàn)镃onnection 在任何時(shí)候都無(wú)法自動(dòng)回收,而Connection一旦回收,Resultset 和Statement 對(duì)象就會(huì)立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關(guān)閉連接,還必須顯式地關(guān)閉Resultset Statement 對(duì)象(關(guān)閉其中一個(gè),另外一個(gè)也會(huì)關(guān)閉),否則就會(huì)造成大量的Statement 對(duì)象無(wú)法釋放,從而引起內(nèi)存泄漏。
3.3.4 內(nèi)部類和外部模塊等的引用內(nèi)部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對(duì)象沒有釋放。對(duì)于程序員而言,自己的程序很清楚,如果發(fā)現(xiàn)內(nèi)存泄漏,自己對(duì)這些對(duì)象的引用可以很快定位并解決,但是現(xiàn)在的應(yīng)用軟件并非一個(gè)人實(shí)現(xiàn),模塊化的思想在現(xiàn)代軟件中非常明顯,所以程序員要小心外部模塊不經(jīng)意的引用,例如程序員A 負(fù)責(zé)A 模塊,調(diào)用了B 模塊的一個(gè)方法如: public void registerMsg(Object b); 這種調(diào)用就要非常小心了,傳入了一個(gè)對(duì)象,很可能模塊B就保持了對(duì)該對(duì)象的引用,這時(shí)候就需要注意模塊B 是否提供相應(yīng)的操作去除引用。 4 預(yù)防和檢測(cè)內(nèi)存漏洞 在了解了引起內(nèi)存泄漏的一些原因后,應(yīng)該盡可能地避免和發(fā)現(xiàn)內(nèi)存泄漏。 (1)好的編碼習(xí)慣。最基本的建議就是盡早釋放無(wú)用對(duì)象的引用,大多數(shù)程序員在使用臨時(shí)變量的時(shí)候,都是讓引用變量在退出活動(dòng)域后,自動(dòng)設(shè)置為null。在使用這種方式時(shí)候,必須特別注意一些復(fù)雜的對(duì)象圖,例如數(shù)組、列、樹、圖等,這些對(duì)象之間有相互引用關(guān)系較為復(fù)雜。對(duì)于這類對(duì)象,GC 回收它們一般效率較低。如果程序允許,盡早將不用的引用對(duì)象賦為null。另外建議幾點(diǎn):在確認(rèn)一個(gè)對(duì)象無(wú)用后,將其所有引用顯式的置為null;當(dāng)類從Jpanel 或Jdialog 或其它容器類繼承的時(shí)候,刪除該對(duì)象之前不妨調(diào)用它的removeall()方法;在設(shè)一個(gè)引用變量為null 值之前,應(yīng)注意該引用變量指向的對(duì)象是否被監(jiān)聽,若有,要首先除去監(jiān)聽器,然后才可以賦空值;當(dāng)對(duì)象是一個(gè)Thread 的時(shí)候,刪除該對(duì)象之前不妨調(diào)用它的interrupt()方法;內(nèi)存檢測(cè)過(guò)程中不僅要關(guān)注自己編寫的類對(duì)象,同時(shí)也要關(guān)注一些基本類型的對(duì)象,例如:int[]、String、char[]等等;如果有數(shù)據(jù)庫(kù)連接,使用try...finally 結(jié)構(gòu),在finally 中關(guān)閉Statement 對(duì)象和連接。 (2)好的測(cè)試工具。在開發(fā)中不能完全避免內(nèi)存泄漏,關(guān)鍵要在發(fā)現(xiàn)有內(nèi)存泄漏的時(shí)候能用好的測(cè)試工具迅速定位問(wèn)題的所在。市場(chǎng)上已有幾種專業(yè)檢查Java 內(nèi)存泄漏的工具,它們的基本工作原理大同小異,都是通過(guò)監(jiān)測(cè)Java 程序運(yùn)行時(shí),所有對(duì)象的申請(qǐng)、釋放等動(dòng)作,將內(nèi)存管理的所有信息進(jìn)行統(tǒng)計(jì)、分析、可視化。開發(fā)人員將根據(jù)這些信息判斷程序是否有內(nèi)存泄漏問(wèn)題。這些工具包括Optimizeit Profiler、JProbe Profiler、JinSight、Rational 公司的Purify 等。記:映像(Reflector)是一個(gè)程序分析自己的能力。java.lang.reflect包提供了獲取關(guān)于字段、構(gòu)造函數(shù)、方法和類的修改器的信息的能力。利用這些信息可以建立和Java Beans組件打交道的工具。可以動(dòng)態(tài)創(chuàng)建組件的特征。堆(heap):棧(stack)與堆(heap)都是Java用來(lái)在Ram中存放數(shù)據(jù)的地方。與C++不同,Java自動(dòng)管理?xiàng):投?,程序員不能直接地設(shè)置?;蚨选5膬?yōu)勢(shì)是,存取速度比堆要快,僅次于直接位于CPU中的寄存器。但缺點(diǎn)是,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。另外,棧數(shù)據(jù)可以共享,堆的優(yōu)勢(shì)是可以動(dòng)態(tài)地分配內(nèi)存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會(huì)自動(dòng)收走這些不再使用的數(shù)據(jù)。但缺點(diǎn)是,由于要在運(yùn)行時(shí)動(dòng)態(tài)分配內(nèi)存,存取速度較慢。連接池:在實(shí)際應(yīng)用開發(fā)中,特別是在WEB應(yīng)用系統(tǒng)中,如果JSP、Servlet或EJB使用JDBC直接訪問(wèn)數(shù)據(jù)庫(kù)中的數(shù)據(jù),每一次數(shù)據(jù)訪問(wèn)請(qǐng)求都必須經(jīng)歷建立數(shù)據(jù)庫(kù)連接、打開數(shù)據(jù)庫(kù)、存取數(shù)據(jù)和關(guān)閉數(shù)據(jù)庫(kù)連接等步驟,而連接并打開數(shù)據(jù)庫(kù)是一件既消耗資源又費(fèi)時(shí)的工作,如果頻繁發(fā)生這種數(shù)據(jù)庫(kù)操作,系統(tǒng)的性能必然會(huì)急劇下降,甚至?xí)?dǎo)致系統(tǒng)崩潰。數(shù)據(jù)庫(kù)連接池技術(shù)是解決這個(gè)問(wèn)題最常用的方法,在許多應(yīng)用程序服務(wù)器(例如:Weblogic,WebSphere,JBoss)中,基本都提供了這項(xiàng)技術(shù),無(wú)需自己編程,但是,深入了解這項(xiàng)技術(shù)是非常必要的?! ?shù)據(jù)庫(kù)連接池技術(shù)的思想非常簡(jiǎn)單,將數(shù)據(jù)庫(kù)連接作為對(duì)象存儲(chǔ)在一個(gè)Vector對(duì)象中,一旦數(shù)據(jù)庫(kù)連接建立后,不同的數(shù)據(jù)庫(kù)訪問(wèn)請(qǐng)求就可以共享這些連接,這樣,通過(guò)復(fù)用這些已經(jīng)建立的數(shù)據(jù)庫(kù)連接,可以克服上述缺點(diǎn),極大地節(jié)省系統(tǒng)資源和時(shí)間?! ?/p> 數(shù)據(jù)庫(kù)連接池的主要操作如下:
(1)建立數(shù)據(jù)庫(kù)連接池對(duì)象(服務(wù)器啟動(dòng))?! ?/p> (2)按照事先指定的參數(shù)創(chuàng)建初始數(shù)量的數(shù)據(jù)庫(kù)連接(即:空閑連接數(shù))?! ?/p> (3)對(duì)于一個(gè)數(shù)據(jù)庫(kù)訪問(wèn)請(qǐng)求,直接從連接池中得到一個(gè)連接。如果數(shù)據(jù)庫(kù)連接池對(duì)象中沒有空閑的連接,且連接數(shù)沒有達(dá)到最大(即:最大活躍連接數(shù)),創(chuàng)建一個(gè)新的數(shù)據(jù)庫(kù)連接?! ?/p> (4)存取數(shù)據(jù)庫(kù)?! ?/p> (5)關(guān)閉數(shù)據(jù)庫(kù),釋放所有數(shù)據(jù)庫(kù)連接(此時(shí)的關(guān)閉數(shù)據(jù)庫(kù)連接,并非真正關(guān)閉,而是將其放入空閑隊(duì)列中。如實(shí)際空閑連接數(shù)大于初始空閑連接數(shù)則釋放連接)?! ?/p> (6)釋放數(shù)據(jù)庫(kù)連接池對(duì)象(服務(wù)器停止、維護(hù)期間,釋放數(shù)據(jù)庫(kù)連接池對(duì)象,并釋放所有連接)。