內存泄漏和內存溢出

1.1 內存泄漏指由於疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏會因爲減少可用內存的數量從而降低計算機的性能。最終,在最糟糕的情況下,過多的可用內存被分配掉導致全部或部分設備停止正常工作,或者應用程序崩潰。

1.2 下面給出了一個簡單的內存泄露的例子:我們循環申請Object對象,並將所申請的對象放入一個Vector中,如果我們僅僅釋放引用本身,那麼Vector仍然引用該對象,所以這個對象對GC來說是不可回收的。因此,如果對象加入到Vector後,還必須從Vector中刪除,最簡單的方法就是將Vector對象設置爲null。
Vectorv=newVector(10);
for(inti=1;i<100;i++)
{ 
		Objecto=newObject();
		v.add(o);
		o=null;
}

2.1 內存溢出是指程序要求的內存,超出了系統所能分配的範圍,從而發生溢出。 
內存溢是指在一個域中輸入的數據超過它的要求而且沒有對此作出處理引發的數據溢出問題,多餘的數據就可以作爲指令在計算機上運行。
2.2 爲了便於理解,我們不妨打個比方:緩衝區溢出好比是將十磅的糖放進一個只能裝五磅的容器裏。一旦該容器放滿了,餘下的部分就溢出在櫃檯和地板上,弄得一團糟。由於計算機程序的編寫者寫了一些編碼,但是這些編碼沒有對目的區域或緩衝區——五磅的容器——做適當的檢查,看它們是否夠大,能否完全裝入新的內容——十磅的糖,結果可能造成緩衝區溢出的產生。當緩衝區溢出時,過剩的信息覆蓋的是計算機內存中以前的內容,除非這些被覆蓋的內容被保存或能夠恢復,否則就會永遠丟失。

-------------------------------------------

顯而易見:過多的內存泄漏必定會造成內存溢出

比如:應用服務器內存長期不合理佔用,內存經常處於高位佔用,很難回收到低位; 應用服務器極爲不穩定,幾乎每兩天重新啓動一次,有時甚至每天重新啓動一次;  應用服務器經常做Full GC(Garbage Collection),而且時間很長,大約需要30-40秒,應用服務器在做Full GC的時候是不響應客戶的交易請求的,非常影響系統性能

1. 內存溢出類型  
    1.1. java.lang.OutOfMemoryError: PermGen space
    JVM管理兩種類型的內存,堆和非堆。堆是給開發人員用的上面說的就是,是在JVM啓動時創建;非堆是留給JVM自己用的,用來存放類的信息的。它和堆不同,運行期內GC不會釋放空間。如果web app用了大量的第三方jar或者應用有太多的class文件而恰好MaxPermSize設置較小,超出了也會導致這塊內存的佔用過多造成溢出,或者tomcat熱部署時侯不會清理前面加載的環境,只會將context更改爲新部署的,非堆存的內容就會越來越多。  PermGen space的全稱是Permanent Generation space,是指內存的永久保存區域,這塊內存主要是被JVM存放Class和Meta信息的,Class在被Loader時就會被放到PermGen space中,它和存放類實例(Instance)的Heap區域不同,GC(Garbage Collection)不會在主程序運行期對PermGen space進行清理,所以如果你的應用中有很CLASS的話,就很可能出現PermGen space錯誤,這種錯誤常見在web服務器對JSP進行pre compile的時候。如果你的WEB APP下都用了大量的第三方jar, 其大小超過了jvm默認的大小(4M)那麼就會產生此錯誤信息了。一個最佳的配置例子:(經過本人驗證,自從用此配置之後,再未出現過tomcat死掉的情況)  set JAVA_OPTS=-Xms800m -Xmx800m -XX:PermSize=128M -XX:MaxNewSize=256m -XX:MaxPermSize=256m

    1.2. java.lang.OutOfMemoryError: Java heap space  
    第一種情況是個補充,主要存在問題就是出現在這個情況中。其默認空間(即-Xms)是物理內存的1/64,最大空間(-Xmx)是物理內存的1/4。如果內存剩餘不到40%,JVM就會增大堆到Xmx設置的值,內存剩餘超過70%,JVM就會減小堆到Xms設置的值。所以服務器的Xmx和Xms設置一般應該設置相同避免每次GC後都要調整虛擬機堆的大小。假設物理內存無限大,那麼JVM內存的最大值跟操作系統有關,一般32位機是1.5g到3g之間,而64位的就不會有限制了。注意:如果Xms超過了Xmx值,或者堆最大值和非堆最大值的總和超過了物理內存或者操作系統的最大限制都會引起服務器啓動不起來。

    垃圾回收GC的角色 :JVM調用GC的頻度還是很高的,主要兩種情況下進行垃圾回收:  當應用程序線程空閒;另一個是java內存堆不足時,會不斷調用GC,若連續回收都解決不了內存堆不足的問題時,就會報out of memory錯誤。因爲這個異常根據系統運行環境決定,所以無法預期它何時出現。根據GC的機制,程序的運行會引起系統運行環境的變化,增加GC的觸發機會。 爲了避免這些問題,程序的設計和編寫就應避免垃圾對象的內存佔用和GC的開銷。顯示調用System.GC()只能建議JVM需要在內存中對垃圾對象進行回收,但不是必須馬上回收, 一個是並不能解決內存資源耗空的局面,另外也會增加GC的消耗。


2. JVM內存區域組成
    2.1. 棧(棧內存):在函數中定義的基本類型變量和對象的引用變量都在函數的棧內存中分配;在函數(代碼塊)中定義一個變量時,java就在棧中爲這個變量分配內存空間,當超過變量的作用域後,java會自動釋放掉爲該變量所分配的內存空間。
棧調整:參數有+UseDefaultStackSize -Xss256K,表示每個線程可申請256k的棧空間,每個線程都有他自己的Stack。
優勢是存取速度比堆要快;
缺點是存在棧中的數據大小與生存期必須是確定的無靈活性。
    2.2. 堆(堆內存):堆內存用來存放由new創建的對象和數組;在堆中分配的內存由java虛擬機的自動垃圾回收器來管理。java堆分爲三個區:New、Old和Permanent。
GC有兩個線程:新創建的對象被分配到New區,當該區被填滿時會被GC輔助線程移到Old區,當Old區也填滿了會觸發GC主線程遍歷堆內存裏的所有對象。Old區的大小等於Xmx減去-Xmn。
優勢是可以動態分配內存大小,生存期也不必事先告訴編譯器,因爲它是在運行時動態分配內存的;
缺點就是要在運行時動態分配內存,存取速度較慢。


3. JVM如何設置虛擬內存
    提示:在JVM中如果98%的時間是用於GC且可用的Heap size 不足2%的時候將拋出此異常信息。
    提示:Heap Size 最大不要超過可用物理內存的80%,一般的要將-Xms和-Xmx選項設置爲相同,而-Xmn爲1/4的-Xmx值。 
    提示:JVM初始分配的內存由-Xms指定,默認是物理內存的1/64;JVM最大分配的內存由-Xmx指定,默認是物理內存的1/4。
    默認空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制;空餘堆內存大於70%時,JVM會減少堆直到-Xms的最小限制。因此服務器一般設置-Xms、-Xmx相等以避免在每次GC 後調整堆的大小。 
    提示:假設物理內存無限大的話,JVM內存的最大值跟操作系統有很大的關係。
    簡單的說就32位處理器雖然可控內存空間有4GB,但是具體的操作系統會給一個限制,
    這個限制一般是2GB-3GB(一般來說Windows系統下爲1.5G-2G,Linux系統下爲2G-3G),而64bit以上的處理器就不會有限制了
    提示:注意:如果Xms超過了Xmx值,或者堆最大值和非堆最大值的總和超過了物理內存或者操作系統的最大限制都會引起服務器啓動不起來。
    提示:設置NewSize、MaxNewSize相等,"new"的大小最好不要大於"old"的一半,原因是old區如果不夠大會頻繁的觸發"主" GC ,大大降低了性能
    JVM使用-XX:PermSize設置非堆內存初始值,默認是物理內存的1/64;
    由XX:MaxPermSize設置最大非堆內存的大小,默認是物理內存的1/4。
    解決方法:手動設置Heap size
    修改TOMCAT_HOME/bin/catalina.bat
    在“echo "Using CATALINA_BASE: $CATALINA_BASE"”上面加入以下行:
    JAVA_OPTS="-server -Xms800m -Xmx800m -XX:MaxNewSize=256m"


4. 內存溢出檢測,如何查找引起內存泄漏的原因一般有兩個步驟:
   第一是安排有經驗的編程人員對代碼進行走查和分析,找出內存泄漏發生的位置,可以安排對系統業務和開發語言工具比較熟悉的開發人員對應用的代碼進行了交叉走查,儘量找出代碼中存在的數據庫連接聲明和結果集未關閉、代碼冗餘等故障代碼。 
   第二是使用專門的內存泄漏測試工具進行測試,檢測Java的內存泄漏。在這裏我們通常使用一些工具來檢查Java程序的內存泄漏問題。市場上已有幾種專業檢查Java內存泄漏的工具,它們的基本工作原理大同小異,都是通過監測Java程序運行時,所有對象的申請、釋放等動作,將內存管理的所有信息進行統計、分析、可視化。開發人員將根據這些信息判斷程序是否有內存泄漏問題。這些工具包括Optimizeit Profiler,JProbe Profiler,JinSight , Rational 公司的Purify等。


5. 幾種典型的Java內存泄漏
5.1 全局集合:
   在大型應用程序中存在各種各樣的全局數據倉庫是很普遍的,比如一個JNDI-tree或者一個session table。在這些情況下,必須注意管理儲存庫的大小。必須有某種機制從儲存庫中移除不再需要的數據。 
   通常有很多不同的解決形式,其中最常用的是一種週期運行的清除作業。這個作業會驗證倉庫中的數據然後清除一切不需要的數據。 
   另一種管理儲存庫的方法是使用反向鏈接(referrer)計數。然後集合負責統計集合中每個入口的反向鏈接的數目。這要求反向鏈接告訴集合何時會退出入口。當反向鏈接數目爲零時,該元素就可以從集合中移除了。 
5.2 緩存 
   緩存一種用來快速查找已經執行過的操作結果的數據結構。因此,如果一個操作執行需要比較多的資源並會多次被使用,通常做法是把常用的輸入數據的操作結果進行緩存,以便在下次調用該操作時使用緩存的數據。緩存通常都是以動態方式實現的,如果緩存設置不正確而大量使用緩存的話則會出現內存溢出的後果,因此需要將所使用的內存容量與檢索數據的速度加以平衡。 
   常用的解決途徑是使用java.lang.ref.SoftReference類堅持將對象放入緩存。這個方法可以保證當虛擬機用完內存或者需要更多堆的時候,可以釋放這些對象的引用。 
5.3 類裝載器 
   Java類裝載器的使用爲內存泄漏提供了許多可乘之機。一般來說類裝載器都具有複雜結構,因爲類裝載器不僅僅是隻與"常規"對象引用有關,同時也和對象內部的引用有關。比如數據變量,方法和各種類。這意味着只要存在對數據變量,方法,各種類和對象的類裝載器,那麼類裝載器將駐留在JVM中。既然類裝載器可以同很多的類關聯,同時也可以和靜態數據變量關聯,那麼相當多的內存就可能發生泄漏。


6. 不健壯代碼的特徵及解決辦法
   1、儘早釋放無用對象的引用。好的辦法是使用臨時變量的時候,讓引用變量在退出活動域後,自動設置爲null,暗示垃圾收集器來收集該對象,防止發生內存泄露。
對於仍然有指針指向的實例,jvm就不會回收該資源,因爲垃圾回收會將值爲null的對象作爲垃圾,提高GC回收機制效率;
   2、我們的程序裏不可避免大量使用字符串處理,避免使用String,應大量使用StringBuffer,每一個String對象都得獨立佔用內存一塊區域;
String str = "aaa"; String str2 = "bbb"; String str3 = str + str2;//假如執行此次之後str ,str2以後再不被調用,那它就會被放在內存中等待Java的gc去回收,程序內過多的出現這樣的情況就會報上面的那個錯誤,建議在使用字符串時能使用StringBuffer就不要用String,這樣可以省不少開銷; 
   3、儘量少用靜態變量,因爲靜態變量是全局的,GC不會回收的;
   4、避免集中創建對象尤其是大對象,JVM會突然需要大量內存,這時必然會觸發GC優化系統內存環境;顯示的聲明數組空間,而且申請數量還極大。
   5、儘量運用對象池技術以提高系統性能;生命週期長的對象擁有生命週期短的對象時容易引發內存泄漏,例如大集合對象擁有大數據量的業務對象的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。
   6、不要在經常調用的方法中創建對象,尤其是忌諱在循環中創建對象。可以適當的使用hashtable,vector 創建一組對象容器,然後從容器中去取那些對象,而不用每次new之後又丟棄
   7、一般都是發生在開啓大型文件或跟數據庫一次拿了太多的數據,造成 Out Of Memory Error 的狀況,這時就大概要計算一下數據量的最大值是多少,並且設定所需最小及最大的內存空間值。


7. 案例
   7.1 jspsmartUpload文件上傳:
   使用jspsmartUpload作文件上傳,現在運行過程中經常出現java.outofMemoryError的錯誤,用top命令看看進程使用情況,發現內存不足2M,花了很長時間,發現是jspsmartupload的問題。把jspsmartupload組件的源碼文件(class文件)反編譯成Java文件,如夢方醒: 
   1. m_totalBytes = m_request.getContentLength();         
   2. m_binArray = new byte[m_totalBytes];       
   問題原因是totalBytes這個變量得到的數極大,導致該數組分配了很多內存空間,而且該數組不能及時釋放。解決辦法只能換一種更合適的辦法,至少是不會引發outofMemoryError的方式解決。
   變量m_totalBytes表示用戶上傳的文件的總長度,這是一個很大的數。如果用這樣大的數去聲明一個byte數組,並給數組的每個元素分配內存空間,而且m_binArray數組不能馬上被釋放,JVM的垃圾回收確實有問題,導致的結果就是內存溢出。
   jspsmartUpload爲什麼要這樣做,有他的原因,根據RFC1867的http上傳標準,得到一個文件流,並不知道文件流的長度。設計者如果想文件的長度,只有操作servletinputstream一次才知道,因爲任何流都不知道大小。只有知道文件長度了,纔可以限制用戶上傳文件的長度。爲了省去這個麻煩,jspsmartUpload設計者直接在內存中打開文件,判斷長度是否符合標準,符合就寫到服務器的硬盤。這樣產生內存溢出,這只是我的一個猜測而已。
   所以編程的時候,不要在內存中申請大的空間,因爲web服務器的內存有限,並且儘可能的使用流操作,例如:

	byte[] mFileBody = new byte[512];    
	        Blob vField= rs.getBlob("FileBody");     
	     InputStream instream=vField.getBinaryStream();    
	     FileOutputStream fos=new FileOutputStream(saveFilePath+CFILENAME);    
	         int b;    
	                      while( (b =instream.read(mFileBody)) != -1){    
	                       fos.write(mFileBody,0,b);    
	                        }    
	       fos.close();    
	     instream.close();
   

   7.2 Swing子窗體
  曾經在剛入行的時候做過一個小的swing程序,用到了java SE,swing,Thread等東東,當初經驗少也沒有做過嚴格的性能測試,布到生產環境用了一段時間後發現那個小程序有時候會拋java.lang.OutofMemoryError異常,就是java的內存溢出。當時也上網查了不少資料,試過一些辦法,代碼也稍微做了些優化,但是有一個問題我始終是找不到解決的方案 - 不知爲什麼子窗體關閉後java的垃圾回收機制無法回收其資源,因爲這個Java程序可能要經常開關一些子窗體,那麼這些子窗體關閉後無法釋放資源就造成了Java程序OutOfMemoryError的潛在的隱患! 
最近無意間在網上看到了一個監控java程序內存使用的工具 - JProbe,馬上回想起那個有關內存溢出的難題,於是我就下載了JProbe8.0.0希望從分析內存入手找到我要的答案。軟件下載安裝後,在安裝目錄裏詳盡的用戶指南(懂點軟件和英語的人很快就能上手),主要是兩個步驟: 
   1.用JProbe Config工具根據提示生成J2SE或者J2EE程序的控制腳本(一個.jpl文件和一個.bat文件),在命令行裏進入.bat文件所在的目錄,然後執行該批處理讓要監控的java程序跑起來 
   2.運行JProbe Console工具,點擊“Attach to Session...”按鈕,彈出java程序的內存實時監控圖表“Runtime Summary”,我們主要是看“Data”卡片裏的內容(注意:第一次使用該軟件可能會遇到一些小問題:比如發佈爲jar包的程序如果運行時會去讀配置文件,從控制腳本啓動的話,可能會發生配置文件找不到的異常,解決辦法是:不要打jar包,直接就用文件夾發佈;還有可能因爲一些殺毒軟件的網絡防火牆導致JProbe無法連接到控制腳本的session,造成監控圖表打不開,解決辦法是:取消防火牆對於JProbe訪問網絡的限制) 
   實時監控圖表“Runtime Summary”如下圖所示:

   可以設置要監視的包或者類,然後點擊“Refresh Runtime Data”按鈕刷新這些對象佔用內存的情況,當你覺得某個類比較可疑的話,你可以在不斷的使用程序的過程中監視它的生命週期,看看它是否像預期的那樣在結束了生命週期後佔用的內存就被釋放。
   衆所周知:java的內存回收是自動進行的,無需程序員干預,我們稱其爲垃圾回收,這個垃圾回收可能是不定期的,就是當程序佔用內存資源比較少的情況下可能jvm的垃圾回收頻率就比較低;反之,java程序消耗內存資源比較多的情況下,垃圾回收的頻率和力度就比較高,這種垃圾回收的不確定性很可能會影響我們的判斷,但我們可以點擊JProbe監控界面右上方的“Request a Garbage Collection”(像一個垃圾桶的圖標)按鈕來向jvm發出垃圾回收的請求,等幾秒後再去點擊“Refresh Runtime Data”,這個時候如果那個預期應該已經銷燬的對象的類名還是沒有從監控界面下方的class列中消失或者其對象數量沒有減少的話(請多試幾次,中間可以夾雜些其他增加程序內存使用的操作以確保jvm確實執行了垃圾回收),那恭喜你!90%的可能性你已經找到了程序的某個缺陷 
   這個查找元兇的過程可能是相當耗時的,是對程序員的耐心的挑戰。我熬了一下午一晚上,功夫不負有心人,基本上把我那個小程序的所有內存溢出的漏洞都找到並補上了。事實告訴我之前那個子窗體關閉後資源無法釋放的根本原因是:子窗體雖然調用了dispose()方法,但是子窗體對象的引用一直都在:或者是被靜態HashMap引用、或者是它的內部子線程類沒有釋放、或者是它的某個事件監聽類沒有釋放(借用JProbe的火眼金睛一檢驗,發現問題真是一大堆啊!),so.我們要徹底釋放某個對象佔用資源的關鍵在於找到並釋放所有對它的引用! 
   下面是我解決具體問題的一些經驗: 
   程序中造成內存溢出可能性最大的是HashMap,Hashtable等等集合類,尤其是靜態的,更是要慎之又慎!!!它們引用的對象可能你感覺已經銷燬了,其實很可能你忘記remove鍵值,而如果這些集合對象還是靜態的掛在其他類裏面,那麼這個引用可能一直都在,借用JProbe測試一下,結果往往出人意料,解決辦法:徹底刪除鍵,remove、clear,如果允許最好把集合對象設爲null 
   對於不再使用的線程對象,如果要徹底殺了它,很多書上都推薦用join方法,我之前也是這樣做的,但後來藉助JProbe工具我吃驚的發現這樣做很可能要殺的線程仍舊好好的活在你日益增大的內存裏,很可能調用了線程的sleep方法後用join方法就會有點問題,解決辦法:在join方法前再加一句執行interrupt方法,不過這個時候可能會有新的問題:執行interrupt方法後你的線程類會拋InterruptedException,上有政策下有對策,加一個開關變量做判斷就能完美解決,可參考下面的代碼: 

/**    
 * <p>Description: 創建線程的內部類</p>    
 * @author cuishen    
 * @version 1.1    
 */    
class NewThread implements Runnable {     
    Thread t;     
    NewThread() {     
        t = new Thread(this, path);     
	        t.start();     
	    }     
	    public void run() {     
	        try {     
	            while(isThreadAlive) {     
	                startMonitor();     
	                Thread.sleep(Long.parseLong(controlList.get(controlList.size() - 1).toString()));     
	            }     
	        } catch (InterruptedException e) {     
	            if(!ifForceInterruptThread) {//開關變量     
	                stopThread(logThread);     
	                String error = "InterruptedException!!!" + path + ": Interrupted,線程異常終止!程序已試圖重啓該線程!!";     
	                System.err.println(error);     
	                LogService.writeLog(error);     
	                createLogThread();     
	            }     
	        }     
	    }     
	}     
	    
	public void createLogThread() {     
	    ifForceInterruptThread = false;//開關變量     
	    logThread = new NewThread();     
	}     
	    
	private void stopThread(NewThread thread) {     
	    try {     
	        thread.t.join(100);     
	    } catch (InterruptedException ex) {     
	        System.out.println("線程終止異常!!!");     
	    } finally {     
	        thread = null;     
	    }     
	}     
	    
	/**    
	 * 關閉並徹底釋放該線程資源的方法    
	 */    
	public void stopThread() {     
	    try {     
	        ifForceInterruptThread = true;//開關變量     
	        isThreadAlive = false;     
	        logThread.t.interrupt();     
	        logThread.t.join(50);     
	    } catch (InterruptedException ex) {     
	        System.out.println("線程終止異常!!!");     
	    } finally {     
	        this.controlList = null;     
	        this.keyList = null;     
	        logThread = null;     
	    }     
	}   

   對於繼承JFrame的窗體類,我們要注意在初始化方法中加入:this.setDefaultCloseOperation(DISPOSE_ON_CLOSE); ,並且注意和其關聯的事件監聽類一律寫成窗體類的內部類,這樣窗體dispose()的時候,這些內部類也一併銷燬,就不會再有什麼莫名其妙的引用了。


----------------------------------------------------

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章