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

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

最先接觸內存泄露仍是上一家,作數字電視中間件的,技術槓槓的。不少技術的思路和見識從這裏而來,呆了兩三年吧,後面到了如今這家。那時個人上司本身編寫了一個C語言內存泄露的檢測類。有幾回用在了一些開源庫的排查。來的這裏,後面就是本身寫檢測類,不止C語言的,C++的也有。檢測的工具也不止這些,後面還會介紹。前端

 

案例分析

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

    

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

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

今天咱們先來看看當年的一個很是嚴重的問題的解決過程,先介紹下背景。windows

    

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

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

    

這個關鍵詞對後面咱們講解會有用。當時這個時候我進這個部門不是好久(之前前端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的平臺後面重構的,是跨平臺的。重構的代碼確定會吸收前面的教訓,因此極少出現了內存泄露。

因此也就形成了我對這個工具的印象不深。但其實咱們也有文檔對它進行過介紹,而後網上也有不少資料,能夠自行查閱。

(四)難點

最後,複雜問題的內存泄露的難點是什麼?

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

例如咱們的案例裏面,是和3、四個模塊,好像都有關。因此雖然自研的工具應該更好用,可是不可能每一個模塊的每一個類都去改下宏,工做量極大。可是後面用umdb也沒有找到緣由。固然正規的查找也仍是須要,事實上umdb仍是提供了很好的線索。

可是最後誰也沒想到是一個不應使用而使用了的配置項!可是爲何仍是發現了,有個很重要的觀點,全局觀

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

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

 在龐大的程序中,程序結構或者系統分析纔是重點和難點。當系統較複雜時候,是否須要所有檢查仍是檢查某個模塊;以及在哪一個時候哪一個地方進行釋放。

 

 

 推薦閱讀:

虛擬內存:分頁技術

如何用巧力解決問題

如何把Bug的偶現變必現

相關文章
相關標籤/搜索