基本含義
記憶體溢出已經是軟體開發歷史上存在了近40年的“老大難”問題,像在“紅色代碼”病毒事件中表現的那樣,它已經成為黑客攻擊企業網路的“罪魁禍首”。 如在一個域中輸入的數據超過了它的要求就會引發數據溢出問題,多餘的數據就可以作為指令在計算機上運行。據有關安全小組稱,作業系統中超過50%的安全漏洞都是由記憶體溢出引起的,其中大多數與微軟的技術有關
。
基本內容
為了便於理解,我們不妨打個比方。緩衝區溢出好比是將十磅的糖放進一個只能裝五磅的容器里。一旦該容器放滿了,餘下的部分就溢出在櫃檯和地板上,弄得一團糟。由於電腦程式的編寫者寫了一些編碼,但是這些編碼沒有對目的區域或緩衝區——五磅的容器——做適當的檢查,看它們是否夠大,能否完全裝入新的內容——十磅的糖,結果可能造成緩衝區溢出的產生。如果打算被放進新地方的數據不適合,溢得到處都是,該數據也會製造很多麻煩。但是,如果緩衝區僅僅溢出,這只是一個問題。到此時為止,它還沒有破壞性。當糖溢出時,櫃檯被蓋住。可以把糖擦掉或用吸塵器吸走,還櫃檯本來面貌。與之相對的是,當緩衝區溢出時,過剩的信息復蓋的是計算機記憶體中以前的內容。除非這些被復蓋的內容被保存或能夠恢復,否則就會永遠丟失。
在丟失的信息里有能夠被程式調用的子程式的列表信息,直到緩衝區溢出發生。另外,給那些子程式的信息——參數——也丟失了。這意味著程式不能得到足夠的信息從子程式返回,以完成它的任務。就像一個人步行穿過沙漠。如果他依賴於他的足跡走回頭路,當沙暴來襲抹去了這些痕跡時,他將迷失在沙漠中。這個問題比程式僅僅迷失方向嚴重多了。入侵者用精心編寫的入侵代碼(一種惡意程式)使緩衝區溢出,然後告訴程式依據預設的方法處理緩衝區,並且執行。此時的程式已經完全被入侵者操縱了。
入侵者經常改編現有的應用程式運行不同的程式。例如,一個入侵者能啟動一個新的程式,傳送秘密檔案(支票本記錄,口令檔案,或財產清單)給入侵者的電子郵件。這就好像不僅僅是沙暴吹了腳印,而且後來者也會踩出新的腳印,將我們的迷路者領向不同的地方,他自己一無所知的地方。
套用簡介
緩衝處理
你屋子裡的門和窗戶越少,入侵者進入的方式就越少……
由於緩衝區溢出是一個編程問題,所以只能通過修復被破壞的程式的代碼而解決問題。如果你沒有原始碼,從上面“堆疊溢出攻擊”的原理可以看出,要防止此類攻擊,我們可以:
1、開放程式時仔細檢查溢出情況,不允許數據溢出緩衝區。由於編程和程式語言的原因,這非常困難,而且不適合大量已經在使用的程式;
2、使用檢查堆疊溢出的編譯器或者在程式中加入某些記號,以便程式運行時確認禁止黑客有意造成的溢出。問題是無法針對已有程式,對新程式來講,需要修改編譯器;
3、經常檢查你的作業系統和應用程式提供商的站點,一旦發現他們提供的補丁程式,就馬上下載並且套用在系統上,這是最好的方法。但是系統管理員總要比攻擊者慢一步,如果這個有問題的軟體是可選的,甚至是臨時的,把它從你的系統中刪除。舉另外一個例子,你屋子裡的門和窗戶越少,入侵者進入的方式就越少。
問題提出
記憶體溢出與資料庫鎖表的問題,可以說是開發人員的噩夢,一般的程式異常,總是可以知道在什麼時候或是在什麼操作步驟上出現了異常,而且根據堆疊信息也很容易定位到程式中是某處出現了問題。記憶體溢出與鎖表則不然,一般現象是操作一般時間後系統越來越慢,直到當機,但並不能明確是在什麼操作上出現的,發生的時間點也沒有規律,查看日誌或查看資料庫也不能定位出問題的代碼。
更嚴重的是記憶體溢出與資料庫鎖表在系統開發和單元測試階段並不容易被發現,當系統正式上線一般時間後,操作的並發量上來了,數據也積累了一些,系統就容易出現記憶體溢出或是鎖表的現象,而此時系統又不能隨意停機或重啟,為修正BUG帶來很大的困難。
本文以筆者開發和支持的多個項目為例,與大家分享在開發過程中遇到的Java記憶體溢出和資料庫鎖表的檢測和處理解決過程。
定義及原因
記憶體溢出是指套用系統中存在無法回收的記憶體或使用的記憶體過多,最終使得程式運行要用到的記憶體大於虛擬機能提供的最大記憶體。為了解決Java中記憶體溢出問題,我們首先必須了解Java是如何管理記憶體的。Java的記憶體管理就是對象的分配和釋放問題。在Java中,記憶體的分配是由程式完成的,而記憶體的釋放是由垃圾收集器(GarbageCollection,GC)完成的,程式設計師不需要通過調用GC函式來釋放記憶體,因為不同的JVM實現者可能使用不同的算法管理GC,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是中斷式執行GC。但GC只能回收無用並且不再被其它對象引用的那些對象所占用的空間。Java的記憶體垃圾回收機制是從程式的主要運行對象開始檢查引用鏈,當遍歷一遍後發現沒有被引用的孤立對象就作為垃圾回收。
引起記憶體溢出的原因有很多種,常見的有以下幾種:
記憶體中載入的數據量過於龐大,如一次從資料庫取出過多數據;
集合類中有對對象的引用,使用完後未清空,使得JVM不能回收;
代碼中存在死循環或循環產生過多重複的對象實體;
使用的第三方軟體中的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系統