程序員的踩坑經驗總結(三):內存泄露


內存泄露,是不是很多程序員揮之不去的噩夢呢,哈哈,我也有過這樣的踩坑經歷,但人都是在踩坑中成長的。。。

最早接觸內存泄露還是上一家,做數字電視中間件的,技術杠杠的。很多技術的思路和見識從這里而來,呆了兩三年吧,后面到了現在這家。那時我的上司自己編寫了一個C語言內存泄露的檢測類。有幾次用在了一些開源庫的排查。來的這里,后面就是自己寫檢測類,不止C語言的,C++的也有。檢測的工具也不止這些,后面還會介紹。

 

案例分析

在現在的公司曾經有段時間,都成了解決內存泄露的專業戶,一旦有內存泄露的問題,都往我這里丟!有圖有真相(為了排版的效果,我把圖片都縮小了)。

    

這是有記錄的一般比較嚴重的,沒記錄的還有更多。記得當時另一個部門嵌入式軟件也在用我的寫的檢測類來排查定位,其中還有一位工程師請我吃了一頓飯:)

而后,寫了總結和PPT文檔,在公司范圍內進行了培訓。工具和經驗傳授出去了,慢慢的這些活就少了!

今天我們先來看看當年的一個非常嚴重的問題的解決過程,先介紹下背景。

    

划重點:客戶端視頻解碼過程中出現嚴重內存泄露,沒幾天就會崩潰,在現場和家里,都可重現。

問題的有多嚴重,現在回憶起來又歷歷在目。現場電話輪番轟炸,領導找人開會,周末加班,好像是歷經了兩周才搞定,投入了包括我在內三個人為主,兩人為輔。人員我在文檔上都有記錄,還有關鍵詞等。

    

這個關鍵詞對后面我們講解會有用。當時這個時候我進這個部門不是很久(以前前端SDK組),年初進來的,對客戶端的業務不是很熟。也是因這個Bug對平台的代碼和業務就慢慢熟悉起來了。

但當看到客戶端的代碼着實嚇我一跳,用龐大一定也不過分。要不后面也不會重構一個新的平台。現在也可以再看上面的關鍵字,客戶端還在用MFC。大也算了,問題是耦合程度也讓人一輩子都不會忘記。我依然記得看到三個模塊之間的相互依賴,是那種三角型的關系!!學數學還是學專業課,我記得有位老師講過,他說多邊形里面三角型是最穩定的,因為要使上超過三分之二的力氣才能破壞它的平衡。所以基本沒法解耦,改60%以上的代碼,又這么龐大?所以后面到了新平台的階段了!同時看到這里你就會理解解決這個問題的難處了!問題還是要解決,先了解業務模塊和原理。

我們來看問題的進一步描述。 

    

划重點:解碼庫是控件實現,渲染庫有智能指針,初步驗證鎖定在后者。

后面接着就是跟蹤分析,首先需要工具,如下圖。

    

第一種是windbg的umdh工具,主要特點運行一段時間可以進行差量分析,哪些類和函數的堆棧使用的變化。這個工具的優點是不需要動代碼也不需要重啟,但不是很准確,只能是了解個大概變化。

第二種就是前面提過的自己寫的檢測工具。這個工具的實現原理是重載new/delete,new重載時需要記錄所在文件和所在行數並加入一鏈表中,delete重載時則只需要剔除相應節點即可。那么系統推出時候,鏈表剩下的節點就是沒有釋放的內存。全部打印出來。用這個方法可以直接定位到某個類的某行!但是缺點是,要加宏控制,C++還好,只需重新定義宏變量即可。

工具也有了,那開始干活吧。可是怎么干活,你得先了解類圖吧,數據流程圖吧,可發現這些資料一律沒有。當時我竟然畫了一系列圖(只截了名稱)。

    

    

    

    

可是經過這么大努力,效果甚微!

    

 

但也許轉機就在拐彎處。

    

 

其實還是柳暗花明,只是能確定在XML的解析庫了。具體哪個位置,不知道!

    

  

最終的結果,也讓我大跌眼鏡!

    

划重點:一個DLL庫根本用不上MFC卻選擇了其中的配置選項!去掉只需加上”windows.h“頭文件即可!有妖在作怪。

真的是妖嗎,你信嗎,我不信!我只信科學:)后面我在ppt培訓內存知識的時候,提出了一個觀點”全身而退“!

    

后面還有專題講解。

案例還沒分析完成,我們回到當時的總結。

    

划重點:指針和內存的使用相當零散、混亂、復雜,沒有文檔,類、模塊的定義模糊以及之間調用關系復雜。最終的原因是配置項的使用,而這個庫竟然還在多處使用!

 

總結和建議

上面的案例是翻新,還是讓我感慨萬千!也許我早已忘了,或許塵封起來了,今天竟然又不得不重新過目一遍甚至多遍。這是找自虐嗎?沒辦法,為了寫這篇文檔:)

其實當時可能沒有現在這么痛苦,當時只有一個念頭,解決問題!事實上,通過這個問題,我也名聲鵲起:)但是人有“后怕”這個玩意,你知道嗎。

我也希望我的余生不要碰到這樣的問題,當然,我想也不會了。如果是我主導的程序是不可能出現這樣的問題了,如果是別人的程序,我丟給他幾個工具,自己找去!

其實經過我手的再加上協助分析的內存泄露的問題應該不下二十個。所以我在這里好好梳理一下,通用的原則

當然,大家不要太擔心,一般的內存泄露,用通用的方法足矣。如果說你碰到像上面這樣棘手的,你可以強烈建議重構。但重構還是不能馬上解決問題,像這種問題出現的急解決又要求快。但是,通用的原則也是同樣適用的,只是你可能要花更多的心思和時間。

(一)進程的內存分布

首先,要做到知己知彼,我們要了解內存的分布。如下圖,一個進程的內存分布。

最下面三個區是編譯好了就固定了,變化的是上面兩個區。

棧區的特點是,向下增長,類似數據結構的棧操作方式LIFO。

堆區的特點是,向上增長,動態分配,和數據結構堆操作方式不同,而類似鏈表。

棧區由編譯器自動分配釋放,連續的,一般32位操作系統默認為1MB。堆區一般由程序員分配釋放,是不連續的!

所以內存泄露指的是堆區數據分配后沒釋放。

當然有個別情況,有釋放也存在內存泄露,跟系統回收有關,也不是這里的重點哈。

(二)如何預防 

1. 早發現,早解決

每寫完一個功能的代碼,可以是函數、或者類、或者模塊都應該進行測試。

如果公司有單元測試工具,那自然最好。如果沒有可以自己寫些測試函數。

這個除了對內存,對一般功能測試、函數接口測試等都是應該的。

程序的可調試性也是考慮一個程序員的功底,個人認為。

2. 有良好的設計

設計是個很大的話題,這里專門是針對內存的建議。

2.1 養成良好的編碼習慣

創建和釋放要集中,在一個類中要配對,如 Init---UnInit,Create---Destroy。

釋放的順序應和分配的順序相反。這個說起來容易,做起來難。

2.2 集中管理

例如使用內存池。內存使用對象比較多,或者使用頻繁,例如像我們對文件的讀寫循環一般就需要使用內存池。

如果只是小量使用,可能就是對象的初始化,或者定義一些文件名,一般用不上內存池,那么也應該集中放在一個函數中,例如上面提到的配對函數。

原則不能太分散了,見過有些不規范的編程,可以叫做“隨用隨調(內存分配)”,這種情況看代碼費勁,往往很容易出各種內存問題。

2.3 用try catch,捕獲異常

對所有的new/malloc、delete/free等相關的函數都應該加上,這在一些檢查工具例如pc-lint有要求的。

這里往往也能捕獲到一些內存越界,踩坑經驗總結(一)的案例二。

2.4 對內存的變化加日志跟蹤

特別是異常情況,例如判斷輸入的緩沖長度和輸出長度。

2.5 DLL動態庫的特別之處

我們以提問的方式來說明,一些注意事項。

(1)DDL的庫內部分配的內存,是否可以在調用者模塊中釋放? 即 A庫分配的內存可以在B庫釋放嗎?

(2)如果不釋放,除了內存泄露外,有沒有其他影響?

 答:(1)模塊間內存使用一黃金原則:誰分配誰釋放。

(2)當系統退出時,該DLL需要5秒的時間來清理資源。也就是說比正常退出延時5秒。

這是幾個親身經歷總結出來的經驗!比本文提到的案例還要早!這是windows系統的現象,不知道現在有沒有改進,不過遵循下規則也是沒毛病的!

(三)如何解決

上面的方法適用於開發階段。而真正到了維護階段,重點不一樣了。

1. 了解程序的流程和設計原理

你要解決一個問題,首先要了解它的來龍去脈。

1.1 主體流程

首先要對程序要有個大體認識,理解業務大體流程和模塊之間的關系。

盡量拿到框架設計圖和類圖,如果沒有簡單畫一畫。

1.2 關鍵細節

數據的流向往往都需要通過緩存作為載體,所以抓住關鍵的對象,這些對象一般使用頻率較高,注意內存指針的移動,可以畫畫時序圖。

看到關鍵細節處,一定要去理解作者的原意,不能靠猜測,否則可能帶出新的問題。這一點適用於一般任意的Bug。所以作者留下文檔的意義也在這里。

1.3 盡量重現,找到規律

找到規律了,我認為就成功一半了。找到規律可以縮小范圍,可以定位到某個功能點或者某個模塊,要是某個類就更好了。

1.4 開源庫的排查

主要排查啟動和退出的時候內存的使用。

我一直認為開源庫的穩定性一般沒有太大問題,因為有很多高手在維護。問題是我們在使用的時候,有時沒有理解他的流程和原理,所以由回到了上面。這里舉兩個小例子說明。

SIP協議庫,還在上一家公司好像是VOIP的一個項目,出現了內存泄露,后面排查是會話的退出有個釋放函數沒有被調用。當時經驗不足,用了C語言的檢測工具,調試時間還是比較久的。 

SNMP開源庫,是在這家公司做一個批量升級工具,出現了內存泄露,當時直接查了下退出的一些函數,一個個釋放函數試試,調試幾番就解決了。

開源庫一般會比較復雜點,我記得這兩個庫的釋放函數都不簡單,又都是C語言寫的,指針飛來飛去的,會把你給看暈,文檔可能不是你想要的,最重要的是你可能只是使用下,沒想過要深入。但是同樣解決起來也是相對比較容易的。 

2.工具

本文案例提到過兩種工具,一種是自研的,可以跨平台。一種是windbg的umdb。各自優缺點也介紹了。

linux下的valgrind我用過一兩次吧,總之用的不多,我們linux的平台后面重構的,是跨平台的。重構的代碼肯定會吸取前面的教訓,所以極少出現了內存泄露。

所以也就造成了我對這個工具的印象不深。但其實我們也有文檔對它進行過介紹,然后網上也有很多資料,可以自行查閱。

(四)難點

最后,復雜問題的內存泄露的難點是什么?

是編寫一個檢測工具,還是工具的熟練使用?我的回答都不是,工具固然重要。但是有了工具,如何使用,真的都能查得出來?

例如我們的案例里面,是和三、四個模塊,好像都有關。所以雖然自研的工具應該更好用,但是不可能每個模塊的每個類都去改下宏,工作量極大。但是后面用umdb也沒有找到原因。當然正規的查找也還是需要,事實上umdb還是提供了很好的線索。

但是最后誰也沒想到是一個不該使用而使用了的配置項!但是為什么還是發現了,有個很重要的觀點,全局觀

再例如我們上面說的DLL庫、開源庫等等,用普通的思維(定勢思維)可能很難理解,但是你站在更高一點,你從外面全局審視一下,你發現就可以理解了。

后續,就這個觀點我還會寫一篇文章。最后,我們回到主題上,總結一下,解決復雜的內存泄露的難點是:

 在龐大的程序中,程序結構或者系統分析才是重點和難點。當系統較復雜時候,是否需要全部檢查還是檢查某個模塊;以及在哪個時候哪個地方進行釋放。

 

 

 推薦閱讀:

虛擬內存:分頁技術

如何用巧力解決問題

如何把Bug的偶現變必現

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM