1. 程式人生 > >程式設計師的踩坑經驗總結(三):記憶體洩露

程式設計師的踩坑經驗總結(三):記憶體洩露

記憶體洩露,是不是很多程式設計師揮之不去的噩夢呢,哈哈,我也有過這樣的踩坑經歷,但人都是在踩坑中成長的。。。

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

 

案例分析

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

    

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

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

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

    

劃重點:客戶端視訊解碼過程中出現嚴重記憶體洩露,沒幾天就會崩潰,在現場和家裡,都可重現。

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

    

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

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

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

    

劃重點:解碼庫是控制元件實現,渲染庫有智慧指標,初步驗證鎖定在後者。

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

    

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

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

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

    

    

    

    

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

    

 

但也許轉機就在拐彎處。

    

 

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

    

  

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

    

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

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

    

後面還有專題講解。

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

    

劃重點:指標和記憶體的使用相當零散、混亂、複雜,沒有文件,類、模組的定義模糊以及之間呼叫關係複雜。最終的原因是配置項的使用,而這個庫竟然還在多處使用!

 

總結和建議

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

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

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

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

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

(一)程序的記憶體分佈

首先,要做到知己知彼,我們要了解記憶體的分佈。如下圖,一個程序的記憶體分佈。

最下面三個區是編譯好了就固定了,變化的是上面兩個區,跟著執行時動態變化的。

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

堆區的特點是,向上增長,動態分配,和資料結構堆操作方式不同,而類似連結串列。

棧區由編譯器自動分配釋放,連續的。堆區一般由程式設計師分配釋放,是不連續的!

所以記憶體洩露指的是堆區資料分配後沒釋放。

當然有個別情況,有釋放也存在記憶體洩露,跟系統回收有關,也不是這裡的重點哈。

(二)如何預防 

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也沒有找到原因。

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

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

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

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

 

 

 推薦閱讀:

虛擬記憶體:分頁技術

如何用巧力解決問題

如何把Bug的偶現變必現

&n