內存溢出

計算機術語

內存溢出(Out Of Memory,簡稱OOM)是指應用系統中存在無法回收的內存或使用的內存過多,最終使得程序運行要用到的內存大於能提供的最大內存。此時程序就運行不了,系統會提示內存溢出,有時候會自動關閉軟體,重啟電腦或者軟體后釋放掉一部分內存又可以正常運行該軟體,而由系統配置、數據流、用戶代碼等原因而導致的內存溢出錯誤,即使用戶重新執行任務依然無法避免。

含義


內存溢出已經是軟體開發歷史上存在了近40年的“老大難”問題,像在“紅色代碼”病毒事件中表現的那樣,它已經成為黑客攻擊企業網路的“罪魁禍首”。如果在一個域中輸入的數據超過了它的要求就會引發數據溢出問題,多餘的數據就可以作為指令在計算機上運行。據有關安全小組稱,操作系統中超過50%的安全漏洞都是由內存溢出引起的,其中大多數與微軟的技術有關。內存溢出錯誤是大數據處理平台的常見錯誤,例如,國際知名的程序開發者問答網站stackoverflow上關於“Hadoop out of memory”的問題超過10000個,在Spark郵件列表上有10%的問題是關於“out of memory”。內存溢出錯誤會導致處理數據的任務失敗,甚至會引發平台崩潰等嚴重後果。對於內存溢出大部分的處理方法是重新執行任務,然而,對於由系統配置、數據流、用戶代碼等原因而導致的內存溢出錯誤,即使用戶重新執行任務依然無法避免。
內存溢出通俗理解就是內存不夠,是指運行程序時要求的內存,超出了系統所能分配的範圍,從而導致發生內存溢出。一般在運行大型軟體時,所需的內存遠遠超出了主機內安裝的內存所承受大小時就會發生這種情況。
當出現內存溢出這種情況,系統一般會提示相關信息,有時候會自動關閉軟體甚至會造成設備卡死等現象,重啟電腦或者軟體后釋放掉一部分內存又可以正常運行該軟體或遊戲一段時間。

內容


內存溢出
內存溢出
為了便於理解,不妨打個比方。緩衝區溢出好比是將十磅的糖放進一個只能裝五磅的容器里。一旦該容器放滿了,餘下的部分就溢出在櫃檯和地板上,弄得一團糟。由於計算機程序的編寫者寫了一些編碼,但是這些編碼沒有對目的區域或緩衝區——五磅的容器——做適當的檢查,看它們是否夠大,能否完全裝入新的內容——十磅的糖,結果可能造成緩衝區溢出的產生。如果打算被放進新地方的數據不適合,溢得到處都是,該數據也會製造很多麻煩。但是,如果緩衝區僅僅溢出,這只是一個問題。到此時為止,它還沒有破壞性。當糖溢出時,櫃檯被蓋住。可以把糖擦掉或用吸塵器吸走,還原櫃檯本來的面貌。與之相對的是,當緩衝區溢出時,過剩的信息覆蓋的是計算機內存中以前的內容。除非這些被覆蓋的內容被保存或能夠恢復,否則就會永遠丟失。
在丟失的信息里有能夠被程序調用的子程序的列表信息,直到緩衝區溢出發生。另外,給那些子程序的信息——參數——也丟失了。這意味著程序不能得到足夠的信息從子程序返回,以完成它的任務。就像一個人步行穿過沙漠。如果他依賴於他的足跡走回頭路,當沙暴來襲抹去了這些痕迹時,他將迷失在沙漠中。這個問題比程序僅僅迷失方向嚴重多了。入侵者用精心編寫的入侵代碼(一種惡意程序)使緩衝區溢出,然後告訴程序依據預設的方法處理緩衝區,並且執行。此時的程序已經完全被入侵者操縱了。
入侵者經常改編現有的應用程序運行不同的程序。例如,一個入侵者能啟動一個新的程序,發送秘密文件(支票本記錄,口令文件,或財產清單)給入侵者的電子郵件。這就好像不僅僅是沙暴吹了腳印,而且後來者也會踩出新的腳印,將我們的迷路者領向不同的地方,他自己一無所知的地方。

應用簡介


緩衝處理

你屋子裡的門和窗戶越少,入侵者進入的方式就越少……
由於緩衝區溢出是一個編程問題,所以只能通過修復被破壞的程序的代碼而解決問題。如果你沒有源代碼,從上面“堆棧溢出攻擊”的原理可以看出,要防止此類攻擊,我們可以:
1、開放程序時仔細檢查溢出情況,不允許數據溢出緩衝區。由於編程和編程語言的原因,這非常困難,而且不適合大量已經在使用的程序;
2、使用檢查堆棧溢出的編譯器或者在程序中加入某些記號,以便程序運行時確認禁止黑客有意造成的溢出。問題是無法針對已有程序,對新程序來講,需要修改編譯器;
3、經常檢查你的操作系統和應用程序提供商的站點,一旦發現他們提供的補丁程序,就馬上下載並且應用在系統上,這是最好的方法。但是系統管理員總要比攻擊者慢一步,如果這個有問題的軟體是可選的,甚至是臨時的,把它從你的系統中刪除。舉另外一個例子,你屋子裡的門和窗戶越少,入侵者進入的方式就越少。

問題提出

內存溢出與資料庫鎖表的問題,可以說是開發人員的噩夢,一般的程序異常,總是可以知道在什麼時候或是在什麼操作步驟上出現了異常,而且根據堆棧信息也很容易定位到程序中是某處出現了問題。內存溢出與鎖表則不然,一般現象是操作一般時間后系統越來越慢,直到死機,但並不能明確是在什麼操作上出現的,發生的時間點也沒有規律,查看日誌或查看資料庫也不能定位出問題的代碼。
更嚴重的是內存溢出與資料庫鎖表在系統開發和單元測試階段並不容易被發現,當系統正式上線一段時間后,操作的併發量上來了,數據也積累了一些,系統就容易出現內存溢出或是鎖表的現象,而此時系統又不能隨意停機或重啟,為修正BUG帶來很大的困難。

解決方法


內存溢出雖然很棘手,但也有相應的解決辦法,可以按照從易到難,一步步的解決。
第一步,就是修改JVM啟動參數,直接增加內存。這一點看上去似乎很簡單,但很容易被忽略。JVM默認可以使用的內存為64M,Tomcat默認可以使用的內存為128MB,對於稍複雜一點的系統就會不夠用。在某項目中,就因為啟動參數使用的默認值,經常報“OutOfMemory”錯誤。因此,-Xms,-Xmx參數一定不要忘記加。
第二步,檢查錯誤日誌,查看“OutOfMemory”錯誤前是否有其它異常或錯誤。在一個項目中,使用兩個資料庫連接,其中專用於發送簡訊的資料庫連接使用DBCP連接池管理,用戶為不將簡訊發出,有意將資料庫連接用戶名改錯,使得日誌中有許多資料庫連接異常的日誌,一段時間后,就出現“OutOfMemory”錯誤。經分析,這是由於DBCP連接池BUG引起的,資料庫連接不上后,沒有將連接釋放,最終使得DBCP報“OutOfMemory”錯誤。經過修改正確資料庫連接參數后,就沒有再出現內存溢出的錯誤。
查看日誌對於分析內存溢出是非常重要的,通過仔細查看日誌,分析內存溢出前做過哪些操作,可以大致定位有問題的模塊。
第三步,安排有經驗的編程人員對代碼進行走查和分析,找出可能發生內存溢出的位置。重點排查以下幾點:
檢查代碼中是否有死循環或遞歸調用。
檢查是否有大循環重複產生新對象實體。
檢查對資料庫查詢中,是否有一次獲得全部數據的查詢。一般來說,如果一次取十萬條記錄到內存,就可能引起內存溢出。這個問題比較隱蔽,在上線前,資料庫中數據較少,不容易出問題,上線后,資料庫中數據多了,一次查詢就有可能引起內存溢出。因此對於資料庫查詢盡量採用分頁的方式查詢。
檢查List、MAP等集合對象是否有使用完后,未清除的問題。List、MAP等集合對象會始終存有對對象的引用,使得這些對象不能被GC回收。
第四步,使用內存查看工具動態查看內存使用情況。某個項目上線后,每次系統啟動兩天後,就會出現內存溢出的錯誤。這種情況一般是代碼中出現了緩慢的內存泄漏,用上面三個步驟解決不了,這就需要使用內存查看工具了。
內存溢出
內存溢出
內存查看工具有許多,比較有名的有:Optimizeit Profiler、JProbeProfiler、JinSight和Java1.5的Jconsole等。它們的基本工作原理大同小異,都是監測Java程序運行時所有對象的申請、釋放等動作,將內存管理的所有信息進行統計、分析、可視化。開發人員可以根據這些信息判斷程序是否有內存泄漏問題。一般來說,一個正常的系統在其啟動完成後其內存的佔用量是基本穩定的,而不應該是無限制地增長的。持續地觀察系統運行時使用的內存的大小,可以看到在內存使用監控窗口中是基本規則的鋸齒形的圖線,如果內存的大小持續地增長,則說明系統存在內存泄漏問題。通過間隔一段時間取一次內存快照,然後對內存快照中對象的使用與引用等信息進行比對與分析,可以找出是哪個類的對象在泄漏。
通過以上四個步驟的分析與處理,基本能處理內存溢出的問題。當然,在這些過程中也需要相當的經驗與敏感度,需要在實際的開發與調試過程中不斷積累。

遊戲問題


有的遊戲在XP SP2系統下會出現內存溢出問題,比如在九陰真經紅色警戒3、穿越火線等遊戲時出現死機、電腦自動重啟等現象,解決方法是將系統升級到SP3或更換XP SP3系統。

常見現象


Android開發為例,在開發過程中經常遇到Android內存溢出的意外情況的發生。
以下是國內外總結造成內存溢出的幾點現象。
1.大量點陣圖的載入
Bitmap代表一張點陣圖文件,擴展名是.bmp或者.dip,它是非壓縮格式,其顯示效果較好,但缺點就是需要佔用大量的存儲空間。它是windows標準格式圖形文件,由點組成,每一個點代表一個像素。每個點可以由多種色彩表示,包括2、4、8、16、24和32位色彩。色彩越高,顯示效果越好,但所佔用的位元組數也就越大。計算一張Bitmap所佔內存大小主要由3個因數有關,即圖片寬度,圖片長度,單位像素所佔用的位元組數。大小=圖像長度*圖片寬度*單位像素佔用的位元組數。有時候我們需要從網路上獲取大量的圖片並且展現在view中,但是如果圖片較大,一次性載入大量Bitmap,那麼程序可用內存會瞬間增長,引起OOM。
2.點陣圖對象沒有及時釋放
當程序中需要操作Bitmap 對象的時候,當它不在被使用的時候,可以調用Bitmap.recycle()方法回收此對象的像素所佔用的內存,如果對Bitmap沒有及時釋放,在程序長期運行過程中,就很有可能造成OOM意外情況的發生。
3.查詢資料庫沒有關閉游標
程序中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor后沒有關閉的情況。如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操作的情況下才會復現內存問題,這樣就會給以後的測試和問題排查帶來困難和風險。
4.構造Adapter時,沒有使用緩存的convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提高了方法:publicView getView(int position,View convertView,ViewGroup parent)來向ListView提供每一個item所需要的view對象。初始時ListView 會從BaseAdapter中根據當前的屏幕布局實例化一定數量的view對象,同時ListView會將這些view對象緩存起來。當向上滾動ListView時,原先位於最上面的list item的view對象會被回收,然後被用來構造新出現的最下面的listitem.這個構造過程就是由getView()方法完成的,getView()的第二個形參View convertVicw就是被緩存起來的listitem的view對象(初始化時緩存中沒有view對象則convertView是null)。如果我們不去使用convertView,而是每次都在getView()中重新實例化一個View對象的話,即浪費資源也浪費時間,也會使得內存佔用越來越大。

原因


造成這種現象的原因通常有兩種:
第一種是由於長期保持某些資源的引用,垃圾回收器無法回收它,從而使該資源不能夠及時釋放,也稱為內存泄露;
另外一種是當需要保存多個耗用內存過大或當載入單個超大的對象時,該對象的大小超過了當前剩餘的可用內存空間。
以Android程序為例:
1.由強引用造成的內存溢出
若所有的引用都是強引用,則大量內存會被佔用,最終導致內存溢出。
解決方法:使用弱引用或軟引用,軟引用的對象在內存不足時可被GC回收,弱引用的對象在垃圾回收時可被回收。
2.由大量圖片顯示導致的內存溢出
為解決由大量圖片顯示造成的內存溢出,可以使用BitmapFactory.Options類,在返回參數時,只返回Bitmap的尺寸大小,而不將其載入到內存中,可有效減少內存溢出。同時在載入完后調用system.gc()通知系統及時回收。
3.從資料庫中取出大量數據造成的內存溢出
檢查在資料庫查詢中,是否有一次獲得全部數據的查詢。一般而言,如果一次取十萬條記錄到內存,就可能引起內存溢出。該問題比較隱蔽,在上線前,資料庫中數據較少,通常運行正常,上線后,資料庫中數據增多,一次查詢即有可能引起內存溢出。因此,對於資料庫查詢,盡量採用分頁的方式查詢。
4.代碼中存在死循環或循環產生過多重複對象實體造成的內存溢出
出現這種情況,只能通過查看日誌找出產生該問題的原因,檢查代碼中是否有死循環、遞歸調用,或大循環重複產生的新對象實體。

避免情況


避免內存溢出的常用方法眾所周知,以Android 開發為例,每個Android應用程序在運行時都有一定的內存限制,限制大小一般為16MB或24MB(視平台而定)。當應用程序在實際運行過程中沒有做到合理、有效利用內存空間,超過該限制大小就會內次溢出。
下面是列舉了國內外在Android應用程序開發過程中應對內存溢出而經常採用的方法。
內存泄露的檢測
內存溢出和內存泄露是兩個不同的現象,內存泄露是指長期保持某些資源的引用,垃圾回收器無法回收它,從而造成該資源不能夠及時釋放,隨著程序運行時間的增加,佔用存儲空間越來越多,致使有效可再利用的存儲空間不足,當儲存別的資源時引發內存溢出。
內存泄露是造成內存溢出的一個很主要的原因。因此,在實際的開發過程中要堅決杜絕內存泄露的現象發生。由於Android應用程序是基於虛擬機的,其內存管理都是由Dalivk代為管理,GC回收不是很及時。如果有一個正常的應用程序在其運行穩定后其內存的佔用量是不會無限制的增長,是保持在一個穩定的水平。
同樣,對任何一個類的對象的使用個數也有一個相對穩定的上限,沒有出現持續增長的情況。當我們持續地觀察某個應用程序運行過程中使用內存的大小和各實例的個數時,如果內存的大小持續增長,則說明系統存在內存泄露情況。比如一個Activity被關掉之後,其內存的引用對象會在下次GC回收的時候通過回收演演算法計算,如果這部分內存已經屬於可回收的對象,那麼這些對象會被一併回收。
在重複開發關閉某個應用程序的時候,內存一直在向上爬升,也就是說每次關閉這個Activity 的時候,有些應該釋放的內存並沒有被釋放掉。
採用二級緩衝機制
每次需要載入圖片的時候,首先從特定的內存中查找。如果內存中沒有再從SD卡文件中查找,如果沒找到,則通過網路獲取。當獲得來自網路數據時,先緩衝到底層由硬引用實現的緩衝中(一級緩衝),同時緩衝到文件中(二級緩衝)。
根據硬引用的特性,當回收垃圾的時候自動執行,人為無法干預,即使拋出OOM錯誤,致使應用系統異常終止,也不會隨意回收具有強引用的對象來解決內存不足的問題。
假如當前的網路狀態很好,下載速度很快的環境中,當快速翻動聊天列表需要快速載入並顯示大量圖片的時候,由於對這些圖片是緩衝在LruCache實現的一級緩衝中的,當內存吃緊的時候一級緩衝自動回收,回收的速度遠小於下載並緩衝圖片速度,這時候就很容易導致OOM的發生。
等比例縮小點陣圖文件
如果點陣圖文件太大,則可以通過設置BitmapFactory.Options.inSampleSize(採樣率)來實現等比例縮小該文件,並且設置BitmapFactory.Options的inJustDecodeBounds為true,先獲取到寬高,這時候點陣圖並不會載入到內存中,然後計算縮放比例再載入點陣圖適應view控制項,這樣可以避免OOM的產生。
優化DalivkVM的堆內存
分配堆(heap)是VM中佔用內存最多的部分,通常是通過動態分配來獲得。其大小處於動態變化中,當堆實際的利用率偏離設定值時,虛擬機會在GC的時候調整堆的大小,從而使實際佔用率呈偏大的趨勢靠攏。
強制回收內存的信息
由於Android是採用Java語言實現,因此Android的內存回收也和Java內存回收一樣的機制:通過GC自動管理內存。該機制是通過不定時檢測是否有不被使用的對象,如果有則回收這些對象,釋放內存。但是GC的回收時不規律的,人為無法控制的。
通常會通過System.gc()方法來強制啟動GC來回收垃圾,以便減小OOM發生的概率。但該方法只是告訴機器回收垃圾,當也有可能不會立刻回收。具體情況取決於機器當時所處的運行情況。