Java程序中的“內存泄漏”問題
Java程序中的“內存泄漏”問題
大多數程序員都知道,使用Java編程語言的一大好處就是,不必再擔心內存的分配和釋放問題。您只須創建對象,當應用程序不再需要這些對象 時,Java 會通過一種稱為“垃圾回收”的機制將這些對象的內存釋放掉。他們認為Java不存在內存泄漏問題,或者認為即使有內存泄漏也不是程序的責 任,而是垃圾回收器(GC)或Java虛擬機(JVM)的問題。但事實真的是這樣嗎?Java真的已經解決了困擾其他編程語言的內存泄露問題了嗎?
一、Java的內存管理機制
在進一步討論之前,我們先了解一下Java的內存管理機制。Java的內存管理就是對象的分配和釋放問題。在Java中,程序員需要通過關鍵字new為每 個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。在Java中,內存的分配是由程序完成的,而內存的釋放是則是由垃圾 回收器決定和執行的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的負擔,這也是Java程序運行速度較慢的原因之一。因為, 垃圾回收器為了能夠正確回收對象,必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等。監視對象狀態是為了準確、及時地釋放對象,而釋 放對象的基本原則就是該對象是否仍被引用。
垃圾收集器的工作是發現應用程序不再需要的對象,并在這些對象不再被訪問或引用時將它們刪除。垃圾收集器從根節點(在 Java 應用程序的整個生存周期 內始終存在的那些類)開始,遍歷所有仍被引用的節點,進行垃圾回收。任何對象只要不再被引用,就符合垃圾回收的條件。垃圾回收器回收這些對象后,它們所占 用的內存資源也就被返回給了Java虛擬機。
Java使用有向圖的方式進行內存管理,可以消除循環引用的問題,例如有三個對象,相互引用,只要它們和根線程不可達,那么垃圾回收器也是可以回收它們 的。這種方式的優點是管理內存的精度很高,但是效率較低。另外一種常用的內存管理技術是使用計數器,例如COM模型采用計數器方式管理構件,它與有向圖相 比,精度較低(很難處理循環引用的問題),但執行效率卻很高。
為了更好理解地垃圾回收器的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關系考慮為圖的有向邊,有向邊從引用對象指向被引對象。每個線程可以作為 一個圖的起始頂點,例如大多程序從main線程開始執行,那么該圖就是以main線程為頂點的一個有向圖。在這個有向圖中,根頂點可達的對象都是有效對 象,如果某個對象不可達,那么垃圾回收器會認為這個對象不再被引用,可以被回收。對于程序的每一個時刻,我們都有一個有向圖表示JVM的內存分配情況。
二、什么是Java中的內存泄露
在C++ 程序中,內存泄漏是指應用程序為某些對象被分配了內存空間,然后卻因為某些原因不可達,以至于被這些對象使用的內存無法被釋放并返還給操作系統,這些內存將永遠收不回來。
令人欣慰的是,這種內存泄露問題在Java程序中并不存在。在Java中,對象使用的內存都由垃圾回收器負責回收的,而Java虛擬機并不存在任何被證實 的內存泄漏問題。實踐證明,垃圾收集器一般能夠精確地判斷哪些對象可被收集,回收它們占用的內存空間并返還給Java 虛擬機。
對于Java來說,內存泄漏是指在程序中存在一些實際上并不需要的對象引用。這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可 以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。一個典型的例子是向一個集合中加入一些對象以便以后使用它們,但是卻沒有在使用完后 從集合中刪除對這些對象的引用。因為集合可以無限制地擴大,并且從來不會變小,所以當向集合中加入了太多的對象(或者是有很多的對象被集合中的元素所引 用)時,就會因為堆空間被填滿而導致內存耗盡。垃圾收集器并不會把這些您認為已經用完的對象當作垃圾進行回收,因為對于垃圾收集器來說,應用程序仍然可以 通過這個集合在任何時候訪問這些對象。
1、靜態集合類引起內存泄露:
在這個例子中,循環申請Object 對象,并將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null),那么Vector
仍然引用該對象,所以這個對象對GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從Vector
中刪除,最簡單的方法就是將Vector對象設置為null。
2、當集合里面的對象屬性被修改后,再調用remove()方法時不起作用。
通過以上分析,可以知道在Java中也有內存泄漏,但范圍比C++要小一些。因為Java從語言上保證,任何對象都是可達的,都由垃圾回收器進行內存的回收管理。
隨著越來越多的服務器程序、嵌入式系統及游戲平臺采用Java技術,出現了較多內存有限、需要長期運行Java應用。內存泄露問題也就變得十分關鍵,即使每次少量內存泄漏,長期運行之后,系統也有面臨內存溢出的危險。
三、典型的內存泄漏問題及解決方法
我們知道了在Java中確實會存在內存泄漏,那么就讓我們看一看幾種典型的泄漏,并試圖找出他們的解決方法。
3.1 全局集合
在大型應用程序中存在各種各樣的全局數據儲存庫是很普遍的,比如一個Session Table。在這些情況下,必須注意管理儲存庫的大小。必須使用某種機制從儲存庫中移除不再需要的數據。
通常有很多不同的解決形式,其中最常用的一種是周期運行的清除作業。這個作業會驗證倉庫中的數據然后清除一切不需要的數據。
另一種管理儲存庫的方法是使用反向鏈接(Referrer)計數。然后集合負責統計集合中每個元素反向鏈接的數目,當反向鏈接數目為零時,該元素就可以從集合中移除了。
3.2 緩存
緩存一種用來快速查找已經執行過的操作結果的數據結構。因此,如果一個操作執行需要比較多的資源并會多次被使用,通常做法是把常用的輸入數據的操
作結果進行緩存,以便在下次調用該操作時使用緩存中的數據。緩存通常都是以動態方式實現的,如果緩存設置不正確而大量使用緩存的話,則會出現內存溢出的后
果,因此需要將所使用的內存容量與檢索數據的速度加以平衡。
常用的解決途徑是使用軟引用或弱引用類將對象放入緩存。這個方法可以保證當虛擬機用完內存或者需要更多堆的時候,可以釋放這些對象的引用。
3.3 類裝載器
Java類裝載器的使用為內存泄漏提供了許多可乘之機。一般來說類裝載器都具有復雜結構,因為類裝載器不僅僅是只與“常規”的對象引用有關,同時也和對象 內部的引用有關。比如數據變量,方法和各種類。這意味著只要存在對數據變量,方法,各種類和對象的引用,那么類裝載器將駐留在Java虛擬機中。既然類裝 載器可以同很多的類關聯,同時也可能和靜態數據變量關聯,那么相當多的內存就可能發生泄漏。
3.4 物理連接
一些物理連接,比如數據庫連接和網絡連接,除非其顯式的關閉了連接,否則是不會自動被GC 回收的。Java數據庫連接一般用 DataSource.getConnection()來創建,當不再使用時必須用Close()方法來釋放。對于ResultSet 和 Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,,因為這些連接是獨立于Java虛擬機的,在任何時候都無法自動 回收,而Connection一旦回收,ResultSet 和Statement 對象就會立即變為NULL。但是如果使用連接池,情況就不一樣了,除 了要顯式地關閉連接,還必須顯式地關閉ResultSet和Statement 對象(關閉其中一個,另外一個也會關閉),否則就會造成大量的 Statement 對象無法釋放,從而引起內存泄漏。
3.5 內部模塊和外部模塊等的引用
內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的后繼類對象沒有釋放。對于程序員而言,自己的程序很清楚,如果發現內存泄漏,自己對這 些對象的引用可以很快定位并解決。但是在大型應用軟件的開發中,整個系統并非一個人實現,個人擔當的可能只是系統的某一機能或某機能的一個模塊。所以程序 員要小心外部模塊不經意的引用,例如程序員A 負責A 模塊,調用了B模塊的一個方法如:public void registerMsg(Object b); 這種調用就要非常小心了,傳入了一個對象,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B是否需要提供相應的去除引用的操作。
四、如何找出內存泄漏
查找內存泄漏一般有兩種方法:一是安排有經驗的編程人員對代碼進行走查和分析,找出內存泄漏發生的位置;二是使用專門的內存泄漏測試工具進行測試。
第一種方法,在代碼走查工作中,可以安排對系統業務和開發語言較熟悉的開發人員對應用的代碼進行了交叉走查,盡量找出代碼中存在的數據庫連接聲明和結果集未關閉、代碼冗余等問題代碼。
第二種方法就是使用專門的內存泄漏工具進行測試。市場上已有專業檢查Java內存泄漏的工具,它們的基本工作原理大同小異,都是通過監測Java程序運行 時,所有對象的申請、釋放等動作,將內存管理的所有信息進行統計、分析、可視化。開發人員將根據這些信息判斷程序是否有內存泄漏問題。常用的工具有 Optimizeit Profiler,JProbe Profiler,JinSight以及Rational公司的Purify等。
五、Java中的幾種引用方式
Java中有幾種不同的引用方式,它們分別是:強引用、軟引用、弱引用和虛引用。下面,我們首先詳細地了解下這幾種引用方式的意義。
強引用
在此之前我們介紹的內容中所使用的引用都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
軟引用(SoftReference)
SoftReference 類的一個典型用途就是用于內存敏感的高速緩存。SoftReference 的原理是:在保持對對象的引用時保證在 JVM 報告內存不足情況之前將清除所有的軟引用。關鍵之處在于,垃圾收集器在運行時可能會(也可能不會)釋放軟可及對象。對象是否被釋放取決于垃圾收集器的算法 以及垃圾收集器運行時可用的內存數量。
弱引用(WeakReference)
WeakReference 類的一個典型用途就是規范化映射(canonicalized mapping)。另外,對于那些生存期相對較長而且重新創建的開銷也不高的對象來說,弱引用也比較有用。關鍵之處在于,垃圾收集器運行時如果碰到了弱可及對象,將釋放 WeakReference 引用的對象。然而,請注意,垃圾收集器可能要運行多次才能找到并釋放弱可及對象。
虛引用(PhantomReference)
PhantomReference 類只能用于跟蹤對被引用對象即將進行的收集。同樣,它還能用于執行 pre-mortem 清除操作。PhantomReference 必須與 ReferenceQueue 類一起使用。需要 ReferenceQueue 是因為它能夠充當通知機制。當垃圾收集器確定了某個對象是虛可及對象時,PhantomReference 對象就被放在它的 ReferenceQueue 上。將 PhantomReference 對象放在 ReferenceQueue 上也就是一個通知,表明 PhantomReference 對象引用的對象已經結束,可供收集了。這使您能夠剛好在對象占用的內存被回收之前采取行動。Reference與ReferenceQueue的配合使用。
GC、Reference與ReferenceQueue的交互
A、 GC無法刪除存在強引用的對象的內存。
B、 GC發現一個只有軟引用的對象內存,那么:
① SoftReference對象的referent 域被設置為null,從而使該對象不再引用heap對象。
② SoftReference引用過的heap對象被聲明為finalizable。
③ 當 heap 對象的 finalize() 方法被運行而且該對象占用的內存被釋放,SoftReference 對象就被添加到它的 ReferenceQueue(如果后者存在的話)。
C、 GC發現一個只有弱引用的對象內存,那么:
① WeakReference對象的referent域被設置為null,從而使該對象不再引用heap對象。
② WeakReference引用過的heap對象被聲明為finalizable。
③ 當heap對象的finalize()方法被運行而且該對象占用的內存被釋放時,WeakReference對象就被添加到它的ReferenceQueue(如果后者存在的話)。
D、 GC發現一個只有虛引用的對象內存,那么:
① PhantomReference引用過的heap對象被聲明為finalizable。
② PhantomReference在堆對象被釋放之前就被添加到它的ReferenceQueue。
值得注意的地方有以下幾點:
1、GC在一般情況下不會發現軟引用的內存對象,只有在內存明顯不足的時候才會發現并釋放軟引用對象的內存。
2、GC對弱引用的發現和釋放也不是立即的,有時需要重復幾次GC,才會發現并釋放弱引用的內存對象。
3、軟引用和弱引用在添加到ReferenceQueue的時候,其指向真實內存的引用已經被置為空了,相關的內存也已經被釋放掉了。而虛引用在添加到ReferenceQueue的時候,內存還沒有釋放,仍然可以對其進行訪問。
代碼示例
通過以上的介紹,相信您對Java的引用機制以及幾種引用方式的異同已經有了一定了解。光是概念,可能過于抽象,下面我們通過一個例子來演示如何在代碼中使用Reference機制。
在以上代碼中,注意⑤⑥兩處地方。假如“hello”對象沒有被回收wf.get()將返回“hello”字符串對象,rq.poll()返回null;而加入“hello”對象已經被回收了,那么wf.get()返回null,rq.poll()返回Reference對象,但是此Reference對象中已經沒有str對象的引用了(PhantomReference則與WeakReference、SoftReference不同)。
引用機制與復雜數據結構的聯合應用
了解了GC機制、引用機制,并配合上ReferenceQueue,我們就可以實現一些防止內存溢出的復雜數據類型。
例如,SoftReference具有構建Cache系統的特質,因此我們可以結合哈希表實現一個簡單的緩存系統。這樣既能保證能夠盡可能多的緩存信息,又可以保證Java虛擬機不會因為內存泄露而拋出OutOfMemoryError。這種緩存機制特別適合于內存對象生命周期長,且生成內存對象的耗時比較長的情況,例如緩存列表封面圖片等。對于一些生命周期較長,但是生成內存對象開銷不大的情況,使用WeakReference能夠達到更好的內存管理的效果。
附SoftHashmap的源碼一份,相信看過之后,大家會對Reference機制的應用有更深入的理解。