複製鏈接
請複製以下鏈接發送給好友

內存溢出

鎖定
內存溢出(Out Of Memory,簡稱OOM)是指應用系統中存在無法回收的內存或使用的內存過多,最終使得程序運行要用到的內存大於能提供的最大內存。此時程序就運行不了,系統會提示內存溢出,有時候會自動關閉軟件,重啓電腦或者軟件後釋放掉一部分內存又可以正常運行該軟件,而由系統配置數據流、用户代碼等原因而導致的內存溢出錯誤,即使用户重新執行任務依然無法避免 [1] 
中文名
內存溢出
外文名
out of memory
領    域
計算機科學
原    因
程序實際運行需要內存的大小不夠
影    響
使程序終止運行
解決方法
關閉軟件、重啓電腦或者軟件、增加內存
簡    稱
OOM

內存溢出簡介

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

內存溢出常見現象

以Android 開發為例,在開發過程中經常遇到Android內存溢出的意外情況的發生。 [3] 
以下是國內外總結造成內存溢出的幾點現象。 [3] 
1.大量位圖的加載
Bitmap代表一張位圖文件,擴展名是.bmp或者.dip,它是非壓縮格式,其顯示效果較好,但缺點就是需要佔用大量的存儲空間。它是windows標準格式圖形文件,由點組成,每一個點代表一個像素。每個點可以由多種色彩表示,包括2、4、8、16、24和32位色彩。色彩越高,顯示效果越好,但所佔用的字節數也就越大。計算一張Bitmap所佔內存大小主要由3個因數有關,即圖片寬度,圖片長度,單位像素所佔用的字節數。大小=圖像長度*圖片寬度*單位像素佔用的字節數。有時候我們需要從網絡上獲取大量的圖片並且展現在view中,但是如果圖片較大,一次性加載大量Bitmap,那麼程序可用內存會瞬間增長,引起OOM。 [4] 
2.位圖對象沒有及時釋放
當程序中需要操作Bitmap 對象的時候,當它不在被使用的時候,可以調用Bitmap.recycle()方法回收此對象的像素所佔用的內存,如果對Bitmap沒有及時釋放,在程序長期運行過程中,就很有可能造成OOM意外情況的發生。 [4] 
3.查詢數據庫沒有關閉遊標
程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對內存的消耗不容易被發現,只有在常時間大量操作的情況下才會復現內存問題,這樣就會給以後的測試和問題排查帶來困難和風險。 [4] 
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對象的話,即浪費資源也浪費時間,也會使得內存佔用越來越大。 [4] 

內存溢出主要原因

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

內存溢出解決方法

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

內存溢出避免內存溢出

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

內存溢出相關概念

內存溢出內存泄露

內存泄露是造成內存溢出的其中一個原因,但是內存泄露不一定會造成內存溢出。簡單來説,內存溢出就是佔用內存太大,超過了系統可以承受的範圍;而內存泄露則是由於對程序運行分配的對象回收不及時甚至於脆沒有被回收,久而久之,則在系統分配的空間裏面產生了很多無用的引用。 [6] 
這種情況下,系統配置容量再多的內存空間都有可能發生內存溢出。當Android中Dalivk啓動GarbageCollection(GC)機制進行垃圾回收的時候,GC會選擇一些它瞭解還存活的對象作為內存遍歷的根節點(GC Roots),比方説thread stack中的變量, JNI中的全局變量,zygote中的對象(class loader加載)等,然後開始對heap進行遍歷。到最後,部分沒有直接或者間接引用到GC Roots的就是需要回收的垃圾,會被GC回收掉。GC只能回收那麼沒有被引用的對象,如果一直引用,當遍歷的時候,系統會默認為該對象仍然處於使用過程中,GC無法回收供其他再次分配使用,但實際上這些被引用的對象對當前應用程序來説,是沒有任何意義的,使得實際上可用的內存空間逐漸縮小。 [6] 
以發生的方式進行分類,內存泄露可以具體分為如下幾類: [7] 
(1) 偶發性內存泄露對造成內存泄露的代碼只是在某些特定的環境或者操作過程下才會發生。一般情況下不會發生這種現象。 [7] 
(2)常發性內存泄露對造成內存泄露的代碼會被多次執行,每次被執行的時候都會導致一塊內存泄露。偶發性和常發性內存泄露是相對而言的。對於使用不同的測試工具和測試算法,常發性可能會變成偶發性內存泄露,或者偶發性內存泄露也會變成常發性內存泄露。 [7] 
(3)一次性內存泄露對造成內存泄露的代碼只會被執行一次。比如,在初始化階段,在類的構造函數中分配內存,但是在執行結束的階段沒有釋放該內存,從而造成內存泄露的意外情況發生。 [7] 
(4)隱式內存泄露程序在運行過程中不停地分配內存,但是沒有及時釋放,而是再等到執行結束的時候才會釋放內存,嚴格地説,這種情況就會中造成內存泄露的發生。例如對於一個服務器,需要運行的時間很長,可能會長達幾百天,如果存在隱式內存泄露,在最壞情況就會發生內存泄露。 [7] 
從用户使用應用程序的角度來看,內存泄露是一種常見的現象,它本身不會產生非常重大的危害,甚至有一部分用户根本感覺不到內存泄露的發生。但是真正危害之處在於,這種內存泄露現象的堆積,最終會消耗盡系統所有的內存。因此,具有良好的編程習慣和採取嚴格的軟件測試對於避免內存泄露是一種非常有效的方式。 [7] 

內存溢出緩衝區溢出

當計算機向緩衝區內填充數據位數時,超過了緩衝區本身的容量,溢出的數據覆蓋在合法數據上。 [7] 
操作系統所使用的緩衝區又被稱為"堆棧". 在各個操作進程之間,指令會被臨時儲存在"堆棧"當中,"堆棧"也會出現緩衝區溢出。 [7] 
注意:緩衝區溢出和內存溢出的區別,前者是溢出後的數據會覆蓋到計算機內存中以前的內容。除非這些被覆蓋的內容被保存或能夠恢復,否則就會永遠丟失。黑客入侵的一種就是用精心編寫的入侵代碼(一種惡意程序)使緩衝區溢出,然後用自己預設的方法處理緩衝區,並且執行,從而達到入侵操縱。而後者內存溢出是系統自身內存有限無法滿足申請需求。 [7] 

內存溢出棧溢出

棧溢出就是緩衝區溢出的一種。 由於緩衝區溢出而使得有用的存儲單元被改寫,往往會引發不可預料的後果。程序在運行過程中,為了臨時存取數據的需要,一般都要分配一些內存空間,通常稱這 些空間為緩衝區。如果向緩衝區中寫入超過其本身長度的數據,以致於緩衝區無法容納,就會造成緩衝區以外的存儲單元被改寫,這種現象就稱為緩衝區溢出。 [7] 
參考資料