關於java內存泄漏的經典文章(一)

一 問題的提出
       Java的一個重要優點就是通過垃圾收集器(Garbage Collection,GC)自動管理內存的回收,程序員不需要通過調用函數來釋放內存。因此,很多程序員認爲Java不存在內存泄漏問題,或者認爲即使 有內存泄漏也不是程序的責任,而是GC或JVM的問題。其實,這種想法是不正確的,因爲Java也存在內存泄露,但它的表現與C++不同。
隨着越來越多的服務器程序採用Java技術,例如JSP,Servlet, EJB等,服務器程序往往長期運行。另外,在很多嵌入式系統中,內存的總量非常有限。內存泄露問題也就變得十分關鍵,即使每次運行少量泄漏,長期運行之後,系統也是面臨崩潰的危險。
二Java是如何管理內存
       爲了判斷Java中是否有內存泄露,我們首先必須瞭解Java是如何管理內存的。Java的內存管理就是對象的分配和釋放問題。分配內存的方式多種多樣,取決於該種語言的語法結構。但不論是哪一種語言的內存分配方式,最後都要返回所分配的內存塊的起始地址,即返回一個指針到內存塊的首地址。在 Java中,程序員需要通過關鍵字new爲每個對象申請內存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由GC決定和執行的。在Java中,內存的分配是由程序完成的,而內存的釋放是由GC完成的,這種收支兩條線 的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是Java程序運行速度較慢的原因之一。因爲,GC爲了能夠正確釋放對象,GC必須監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。爲 了更好理解GC的工作原理,我們可以將對象考慮爲有向圖的頂點,將引用關係考慮爲圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作爲一 個圖的起始頂點,例如大多程序從main進程開始執行,那麼該圖就是以main進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對 象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖爲有向圖),那麼我們認爲這個(這些)對象不再被引用,可以被GC回收。
       以下,我們舉一個例子說明如何用有向圖表示內存管理。對於程序的每一個時刻,我們都有一個有向圖表示JVM的內存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。
       ava使 用有向圖的方式進行內存管理,可以消除引用循環的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那麼GC也是可以回收它們的。這種方式的優 點是管理內存的精度很高,但是效率較低。另外一種常用的內存管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很 難處理循環引用的問題),但執行效率很高
       在 Java 中,所有對象都駐留在堆內存,因此局部對象就不存在。當你創建一個對象時,Java 虛擬機(JVM)爲該對象分配內存、調用構造器並開始跟蹤你使用的對象。當你停止使用一個對象(就是說,當沒有對該對象有效的引用時),JVM 通過垃圾回收器將該對象標記爲釋放狀態。當垃圾回收器要釋放一個對象的內存時,它首先調用該對象的finalize() 方法(如果該對象定義了此方法的話)。垃圾回收器以獨立的低優先級的方式運行,所以只有當其他線程都掛起等待內存釋放的情況出現時,它纔開始釋放對象的內存。
三java垃圾收集器
       垃圾收集器線程是一種低優先級的線程,在一個Java程序的生命週期中,它只有在內存空閒的時候纔有機會運行。
       垃圾收集器的特點和它的執行機制: 垃圾收集器系統有自己的一套方案來判斷哪個內存塊是應該被回收的,哪個是不符合要求暫不回收的。垃圾收集器在一個Java程序中的執行是自動的,不能強制執行,即使程序員能明確地判斷出有一塊內存已經無用了,是應該回收的,程序員也不能強制垃圾收集器回收該內存塊。程序員唯一能做的就是通過調用System. gc 方法來"建議"執行垃圾收集器,但其是否可以執行,什麼時候執行卻都是不可知的。這也是垃圾收集器的最主要的缺點。當然相對於它給程序員帶來的巨大方便性而言,這個缺點是瑕不掩瑜的。
       垃圾收集器的工作是發現應用程序不再需要的對象,並在這些對象不再被訪問或引用時將它們刪除。從根節點(在java應用程序的整個生存週期內始終存在的那些類)開始,遍歷被應用的所有節點進行清除。在它遍歷這些節點的同時,它跟蹤哪些對象當前正被引用着。任何類只要不再被引用,它就符合垃圾收集的條件。當刪除這些對象後,就將它們所佔用的內存資源返回給jvm
(1)、垃圾收集器的主要特點:
1.垃圾收集器的工作目標是回收已經無用的對象的內存空間,從而避免內存滲漏體的產生,節省內存資源,避免程序代碼的崩潰。
2.垃圾收集器判斷一個對象的內存空間是否無用的標準是:如果該對象不能再被程序中任何一個"活動的部分"所引用,此時我們就說,該對象的內存空間已經無用。所謂"活動的部分",是指程序中某部分參與程序的調用,正在執行過程中,尚未執行完畢。
3.垃圾收集器線程雖然是作爲低優先級的線程運行,但在系統可用內存量過低的時候,它可能會突發地執行來挽救內存資源。當然其執行與否也是不可預知的。
4.垃圾收集器不可以被強制執行,但程序員可以通過調用System. gc方法來建議執行垃圾收集器。
5.不能保證一個無用的對象一定會被垃圾收集器收集,也不能保證垃圾收集器在一段Java語言代碼中一定會執行。因此在程序執行過程中被分配出去的內存空間可能會一直保留到該程序執行完畢,除非該空間被重新分配或被其他方法回收。由此可見,完全徹底地根絕內存滲漏體的產生也是不可能的。但是請不要忘記,Java的垃圾收集器畢竟使程序員從手工回收內存空間的繁重工作中解脫了出來。設想一個程序員要用CC++來編寫一段10萬行語句的代碼,那麼他一定會充分體會到Java的垃圾收集器的優點!
6.同樣沒有辦法預知在一組均符合垃圾收集器收集標準的對象中,哪一個會被首先收集。
7.循環引用對象不會影響其被垃圾收集器收集。
8.可以通過將對象的引用變量(reference variables,即句柄handles)初始化爲null值,來暗示垃圾收集器來收集該對象。但此時,如果該對象連接有事件監聽器(典型的 AWT組件),那它還是不可以被收集。所以在設一個引用變量爲null值之前,應注意該引用變量指向的對象是否被監聽,若有,要首先除去監聽器,然後纔可以賦空值。
9.每一個對象都有一個finalize( )方法,這個方法是從Object類繼承來的。
10finalize( )方法用來回收內存以外的系統資源,就像是文件處理器和網絡連接器。該方法的調用順序和用來調用該方法的對象的創建順序是無關的。換句話說,書寫程序時該方法的順序和方法的實際調用順序是不相干的。請注意這只是finalize( )方法的特點。
11.每個對象只能調用finalize( )方法一次。如果在finalize( )方法執行時產生異常exception),則該對象仍可以被垃圾收集器收集。
12.垃圾收集器跟蹤每一個對象,收集那些不可到達的對象(即該對象沒有被程序的任何"活的部分"所調用),回收其佔有的內存空間。但在進行垃圾收集的時候,垃圾收集器會調用finalize( )方法,通過讓其他對象知道它的存在,而使不可到達的對象再次"復甦"爲可到達的對象。既然每個對象只能調用一次finalize( )方法,所以每個對象也只可能"復甦"一次。
13finalize( )方法可以明確地被調用,但它卻不能進行垃圾收集。
14finalize( )方法可以被重載(overload),但只有具備初始的finalize( )方法特點的方法纔可以被垃圾收集器調用。
15.子類的finalize( )方法可以明確地調用父類的finalize( )方法,作爲該子類對象的最後一次適當的操作。但Java編譯器卻不認爲這是一次覆蓋操作(overriding),所以也不會對其調用進行檢查。
16.當finalize( )方法尚未被調用時,System. runFinalization( )方法可以用來調用finalize( )方法,並實現相同的效果,對無用對象進行垃圾收集。
此時容易引起一個誤解——大家可能想finalize() 方法是安全的,這時一些重要的事情需要注意:
首先,只有當垃圾回收器釋放該對象的內存時,纔會執行finalize()。如果在 Applet 或應用程序退出之前垃圾回收器沒有釋放內存,垃圾回收器將不會調用finalize() 其次,除非垃圾回收器認爲Applet或應用程序需要額外的內存,否則它不會試圖釋放不再使用的對象的內存。換句話說,有可能出現這樣的情況:一個 Applet 給少量的對象分配了內存,但沒有造成嚴重的內存需求,於是垃圾回收器沒有釋放這些對象的內存程序就退出了。 顯然,如果爲某個對象定義了finalize() 方法,JVM可能不會調用它,因爲垃圾回收器不曾釋放過那些對象的內存。即使調用System.gc() 也可能不會起作用,因爲它僅僅是給 JVM 的一個建議而不是命令,所以finalize()方法的作用也就不是那麼明顯。Java 1.1中有一個System.runFinalizersOnExit()方法部分地解決了這個問題。(不要將這個方法與 Java1.0中的System.runFinalizations()方法相混淆。)不象System.gc() 方法那樣,System.runFinalizersOnExit()方法並不立即試圖啓動垃圾回收器。而是當應用程序或 Applet 退出時,它調用每個對象的finalize()方法。 結論:不應當依靠垃圾回收器或finalize() 來執行你的 Applet 和應用程序的資源清除工作。取而代之,應當使用確定的方法來清除那些資源或創建一個try...finally 塊(或類似的機制)來實現。
17.當一個方法執行完畢,其中的局部變量就會超出使用範圍,此時可以被當作垃圾收集,但以後每當該方法再次被調用時,其中的局部變量便會被重新創建。
18Java語言使用了一種"標記交換區的垃圾收集算法"。該算法會遍歷程序中每一個對象的句柄,爲被引用的對象做標記,然後回收尚未做標記的對象。所謂遍歷可以簡單地理解爲"檢查每一個"
19Java語言允許程序員爲任何方法添加finalize( )方法,該方法會在垃圾收集器交換回收對象之前被調用。但不要過分依賴該方法對系統資源進行回收和再利用,因爲該方法調用後的執行結果是不可預知的。 通過以上對垃圾收集器特點的瞭解,你應該可以明確垃圾收集器的作用,和垃圾收集器判斷一塊內存空間是否無用的標準。簡單地說,當你爲一個對象賦值爲null並且重新定向了該對象的引用者,此時該對象就符合垃圾收集器的收集標準。
        典型地,GC不會自動執行,直到程序需要的內存比當前可用內存多時才調用,此時,jvm將首先嚐試激活GC以得到更多的可用內存,如果仍得不到充足的可用內存,jvm將轉向從操作系統申請更多的內存,直到最終超過分配的最大內存而導致java.lang.OutOfMemoryError
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章