內存泄漏

卡頓顯示的假象

內存泄漏(Memory Leak)是指程序中已動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。

簡介


內存泄漏缺陷具有隱蔽性、積累性的特徵,比其他內存非法訪問錯誤更難檢測。因為內存泄漏的產生原因是內存塊未被釋放,屬於遺漏型缺陷而不是過錯型缺陷。此外,內存泄漏通常不會直接產生可觀察的錯誤癥狀,而是逐漸積累,降低系統整體性能,極端的情況下可能使系統崩潰。
隨著計算機應用需求的日益增加,應用程序的設計與開發也相應的日趨複雜,開發人員在程序實現的過程中處理的變數也大量增加,如何有效進行內存分配和釋放,防止內存泄漏的問題變得越來越突出。例如伺服器應用軟體,需要長時間的運行,不斷的處理由客戶端發來的請求,如果沒有有效的內存管理,每處理一次請求信息就有一定的內存泄漏。這樣不僅影響到伺服器的性能,還可能造成整個系統的崩潰。因此,內存管理成為軟體設計開發人員在設計中考慮的主要方面

泄漏原因


在C語言中,從變數存在的時間生命周期角度上,把變數分為靜態存儲變數和動態存儲變數兩類。靜態存儲變數是指在程序運行期間分配了固定存儲空間的變數而動態存儲變數是指在程序運行期間根據實際需要進行動態地分配存儲空間的變數。在內存中供用戶使用的內存空間分為三部分:
● 程序存儲區
● 靜態存儲區
● 動態存儲區
程序中所用的數據分別存放在靜態存儲區和動態存儲區中。靜態存儲區數據在程序的開始就分配好內存區,在整個程序執行過程中它們所佔的存儲單元是固定的,在程序結束時就釋放,因此靜態存儲區數據一般為全局變數。動態存儲區數據則是在程序執行過程中根據需要動態分配和動態釋放的存儲單元,動態存儲區數據有三類函數形參變數、局部變數和函數調用時的現場保護與返回地址。由於動態存儲變數可以根據函數調用的需要,動態地分配和釋放存儲空間,大大提高了內存的使用效率,使得動態存儲變數在程序中被廣泛使用。
開發人員進行程序開發的過程使用動態存儲變數時,不可避免地面對內存管理的問題。程序中動態分配的存儲空間,在程序執行完畢后需要進行釋放。沒有釋放動態分配的存儲空間而造成內存泄漏,是使用動態存儲變數的主要問題。一般情況下,開發人員使用系統提供的內存管理基本函數,如malloc、recalloc、calloc、free等,完成動態存儲變數存儲空間的分配和釋放。但是,當開發程序中使用動態存儲變數較多和頻繁使用函數調用時,就會經常發生內存管理錯誤,例如:
● 分配一個內存塊並使用其中未經初始化的內容;
● 釋放一個內存塊,但繼續引用其中的內容;
● 子函數中分配的內存空間在主函數出現異常中斷時、或主函數對子函數返回的信息使用結束時,沒有對分配的內存進行釋放;
● 程序實現過程中分配的臨時內存在程序結束時,沒有釋放臨時內存。內存錯誤一般是不可再現的,開發人員不易在程序調試和測試階段發現,即使花費了很多精力和時間,也無法徹底消除。
產生方式的分類
以產生的方式來分類,內存泄漏可以分為四類:
● ● 常發性內存泄漏:發生內存泄漏的代碼會被多次執行到,每次被執行時都會導致一塊內存泄漏。
● ● 偶發性內存泄漏:發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。
● ● 一次性內存泄漏:發生內存泄漏的代碼只會被執行一次,或者由於演演算法上的缺陷,導致總會有一塊且僅有一塊內存發生泄漏。
● ● 隱式內存泄漏:程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這裡並沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但是對於一個伺服器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏為隱式內存泄漏。從用戶使用程序的角度來看,內存泄漏本身不會產生什麼危害,作為一般的用戶,根本感覺不到內存泄漏的存在。真正有危害的是內存泄漏的堆積,這會最終耗盡系統所有的內存。從這個角度來說,一次性內存泄漏並沒有什麼危害,因為它不會堆積,而隱式內存泄漏危害性則非常大,因為較之於常發性和偶發性內存泄漏它更難被檢測到。

檢測方法


無論是C還是C++程序,運行時候的變數主要有三種分配方式:堆分配、棧分配、全局和靜態內存分配。內存泄漏主要是發生在堆內存分配方式中,即“配置了內存后,所有指向該內存的指針都遺失了”,若缺乏語言這樣的垃圾回收機制,這樣的內存片就無法歸還系統。因為內存泄漏屬於程序運行中的問題,無法通過編譯識別,所以只能在程序運行過程中來判別和診斷。下面將介紹幾種常用的內存檢測方法,每種方法均以現有的內存檢測工具為分析範例,並對各種方法進行比較。
靜態分析技術
靜態分析技術就是直接分析程序的源代碼或機器代碼,獲得一些有用的信息,而並不運行程序本身。目前有許多靜態分析的工具,編譯器就屬於這一類,它讀入源程序代碼,對源程序進行詞法和語法分析,進行數據類型的檢查以及一些優化的分析等,以此來提高程序的質量與運行效率。這類靜態的分析工具僅僅是讀入程序代碼進行相關的分析,而並不進行其它額外的操作,如修改源程序代碼等。
LCLink是一種通過對源代碼及添加到源代碼中特定格式的註釋說明進行靜態分析的程序理解和檢錯工具,的檢查對象是源程序,能檢查出的內存錯誤有內存分配釋放故障、空指針的錯誤使用、使用未定義或已被釋放的內存等程序錯誤。
LCLink重點分析兩類內存釋放錯誤:
● 試圖釋放某內存塊,該內存塊有兩個或兩個以上的有效指針指向它。
● 試圖釋放某內存塊,該內存塊沒有任何有效指針指向它。
解決此類內存錯誤的方法是規定分配某塊內存時返回的指針必須釋放該內存。使用註釋表示某指針是唯一指向某內存塊的指針,使用註釋表示被調用函數可能釋放函數參數指向的內存塊或創建新的指針指向該內存塊。
源代碼插裝技術
圖1
為了獲得被測程序的動態執行信息,需要對其進行跟蹤,一般使用插裝方法。所謂插裝就是在保持被測程序的邏輯完整性的基礎上,在被測程序的特定部位插入一段檢測程序又稱探針函數,通過探針的執行拋出程序的運行特徵數據。基於這些特徵數據分析,可以獲得程序的控制流及數據流信息,進而獲得邏輯覆蓋等動態信息,這樣就可以在被測程序執行的過程中動態地同步執行程序的檢測工作。插裝方法又分為源代碼級程序插裝和目標代碼級程序插裝。源代碼插裝測試必須在靜態測試部分獲得的被測程序的結構信息、靜態數據信息、控制流信息等基礎上,應用插裝技術向被測程序中的適當位置植入相應類型的探針,通過運行帶有探針的被測程序而獲得程序運行的動態數據。源代碼插裝要通過運行被測程序來測定程序的各種指標,如覆蓋率、時間性能、內存使用等等,實現源代碼插裝的關鍵技術是藉助於插入到源程序中的監控語句來收集執行信息,以達到揭示程序內部行為和特性的目的。
基於源代碼插裝的動態測試框架分為個主要的階段:
● 插裝交互與動態測試信息分析;
● 插裝階段;
● 插裝庫製作階段;
● 測試實施階段。
插裝交互與動態測試信息分析是軟體測試工具與用戶交互的界面。用戶通過該界面選擇要進行動態測試的程序模塊,拓撲產生相應的插裝選擇記錄文件。用戶還可以通過該交互界而瀏覽動態測試結果信息,在軟體測試工具的實現上,採用可視化的方式顯示這些動態信息。插裝階段實現了在被測程序中植入探針,並生成帶有插裝信息的源文件。在此過程中,首先將被測程序經過預處理展開為不包含宏、條件編譯和頭文件的文件格式。然後,按照一定的插裝策略,根據前面生成的插裝選擇記錄文件,將探針函數載入到該文件中,最後生成插裝后的程序。插裝庫製作階段的目的是生成插裝庫中的探針函數,它含有插裝語句調用的函數及其函數的定義。顯然,插裝過程中生成的目標文件中含有探針函數的樁,而探針函數的實現恰恰在本過程完成。需要指出的是,插裝庫的製作過程是獨立於動態測試過程之外的,可以與軟體測試工具開發同步。測試實施階段將插裝過程生成的文件與插裝庫製作過程生成的插裝靜態庫連接生成帶有插裝信息的可執行文件,選取測試用例,運行該程序,可以獲得被測程序的動態跟蹤信息。
在以上四個階段中,其中的插裝交互與動態測試信息分析與測試實施階段是測試人員的可視部分,通過這兩部分,用戶與系統交互,完成測試工作。而插裝階段與插裝庫製作階段對測試人員是不可見的,在後台完成,對於用戶而言,這兩部分是完全透明的。在性能方面,採用插裝方法應盡量減少插裝開銷。為了達到不同的統計目的如語句覆蓋、分支覆蓋等,應盡量減少插裝次數。若能僅僅插裝一次就能完成多種類型的統計,則可使插裝代碼得到優化。此外,應盡量減少插裝代碼的數量,減少插裝代碼的運行次數,從而達到減小插裝代碼運行開銷的目的。特別是對於一些實時系統的測試,在這方面的要求尤為苛刻。一個運行時錯誤檢測工具,能夠自動檢測一應用中大量的編程和運行時錯誤。通過使用源碼插裝和運行時指針跟蹤的專利技術,在編譯時,附十插入測試和分析代碼,它建立一個有關程序中各種對象的資料庫。然後在運行時通過檢查數據值和內存引用驗證對象的一致性和正確性。使用這些技術,包括變異測試技術等,一能夠檢查和測試用戶的代碼,精確定位錯誤的準確位置並給出詳細的診斷信息。十十能夠可視化實時內存操作,優化內存演演算法。還能執行覆蓋性分析,清楚地指示那些代碼已經測試過。將集成到開發環境中,能夠極大地減少調試時間並有效地防止錯誤。檢驗每一次內存操作的有效性,包括靜態全局和堆棧以及動態分配內存的操作。葉有兩種運行模式。監護模式下用戶可以快速檢測代碼中的錯誤,不需要對代碼作任何插裝和處理源碼插裝模式則進行徹底地代碼檢測。
目標代碼插裝技術
目標代碼插裝實現主要分為預處理、測試執行和結果匯總個階段,工作流程如圖2所示,系統主要工作是圍繞斷點而進行的。在預處理階段,首先靜態分析被測程序的目標代碼,查找待測程序中源代碼各語句、函數入口點在目標代碼中的對`應位置,然後在相應位置插入斷點在測試執行階段,啟動調試進程,當被測程序執行到斷點處時,響應斷點信息,在相應的斷點處完成相應的統計操作在結果匯總階段,根據各斷點處的統計結果,按不同的統計角度進行歸併、綜合得到最終的統計數據。
被測代碼預處理
在測試預處理階段對被測程序的目標代碼進行分析,可以獲得目標代碼與源代碼中語句、函數的對應關係。在目標代碼中為相對應的源代碼的每條語句及每個函數的入口點插入斷點。對於第三方代碼,只要其目標代碼格式與下生成的目標代碼格式一致,我們就可以用與分析用戶代碼同樣的方法獲取信息。獲取斷點的信息后,為所有的斷點建立斷點鏈表,同時建立語句及函數的信息鏈表,供隨後的測試執行階段存儲信息。預處理流程如圖3所示。
測試執行階段
利用OCI技術,我們把測試執行看作是一個在被測進程和檢測進程間不斷切換的過程。每當被測進程遇到斷點,就會將自身掛起,同時發送消息喚醒檢測進程,檢測進程根據當前斷點的地址在斷點鏈表中查找相應節點,並查找對應的語句或函數信息,記錄該語句或函數的執行次數、到達或離開的時刻,供以後統計之用。然後,將插入的斷點信息去除,恢復原來的指令,轉入被測進程繼續執行。在轉入被測進程之前,必須將上一個斷點處的斷點恢復上一個斷點處的斷點在指令運行時被去除了。具體流程如圖4所示。
數據統計與結果匯總
根據各斷點處的統計結果,按不同的統計角度進行歸併、綜合,進行覆蓋率及各種時間的計算,得到最終的統計數據。是公司出品的一種軟體測試和質量保證工具,它能檢測程序內存泄漏和內存訪問衝突等錯誤。使用目標碼插裝技術,在編譯器生成的目標碼中直接插入特殊的檢查指令實現對內存錯誤的檢測。在程序的所有代碼中插入這些檢查邏輯,包括第三方目標碼庫,並且驗證系統調用的介面。目標碼插裝技術分為鏈接前插裝和鏈接后插裝兩種插裝方法。使用如圖5所示的鏈接前插裝法。檢查插裝后程序的每個內存讀寫動作,跟蹤內存使用情況,使用類似垃圾收集器的技術來檢查內存泄漏。垃圾收集機制分為兩階段垃圾檢測和垃圾回收。為了不影響程序的執行速度,提供了一個可調用的垃圾檢測器,使用類似於保守式垃圾收集演演算法,即標記一清除演演算法。在標記階段,遞歸地從數據段、堆棧段到數據堆跟蹤分析指針,並使用標準保守式方法為所有被引用的內存塊做標記。在清除階段,逐步訪問數據堆,並報告已分配但程序不再引用的內存塊,即程序的內存泄漏。

名詞解釋


也稱作“存儲滲漏”
用動態存儲分配函數動態開闢的空間,在使用完畢后未釋放,結果導致一直佔據該內存單元。直到程序結束。即所謂內存泄漏。

簡要概念


簡單的說就是申請了一塊內存空間,使用完畢后沒有釋放掉。它的一般表現方式是程序運行時間越長,佔用內存越多,最終用盡全部內存,整個系統崩潰。由程序申請的一塊內存,且沒有任何一個指針指向它,那麼這塊內存就泄露了。

泄漏的分類


以發生的方式來分類,內存泄漏可以分為4類:
(1).常發性內存泄漏。發生內存泄漏的代碼會被多次執行到,每次被執行的時候都會導致一塊內存泄漏。
(2).偶發性內存泄漏。發生內存泄漏的代碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測內存泄漏至關重要。
(3).一次性內存泄漏。發生內存泄漏的代碼只會被執行一次,或者由於演演算法上的缺陷,導致總會有一塊僅且一塊內存發生泄漏。比如,在類的構造函數中分配內存,在析構函數中卻沒有釋放該內存,所以內存泄漏只會發生一次。
(4).隱式內存泄漏。程序在運行過程中不停的分配內存,但是直到結束的時候才釋放內存。嚴格的說這裡並沒有發生內存泄漏,因為最終程序釋放了所有申請的內存。但是對於一個伺服器程序,需要運行幾天,幾周甚至幾個月,不及時釋放內存也可能導致最終耗盡系統的所有內存。所以,我們稱這類內存泄漏為隱式內存泄漏。
從用戶使用程序的角度來看,內存泄漏本身不會產生什麼危害,作為一般的用戶,根本感覺不到內存泄漏的存在。真正有危害的是內存泄漏的堆積,這會最終消耗盡系統所有的內存。從這個角度來說,一次性內存泄漏並沒有什麼危害,因為它不會堆積,而隱式內存泄漏危害性則非常大,因為較之於常發性和偶發性內存泄漏它更難被檢測到。

具體表現


內存泄漏或者是說,資源耗盡后,系統會表現出什麼現象哪?
cpu資源耗盡:估計是機器沒有反應了,鍵盤,滑鼠,以及網路等等。這個在windows上經常看見,特別是中了毒。
進程id耗盡:沒法創建新的進程了,串口或者telnet都沒法創建了。
硬碟耗盡:機器要死了,交換內存沒法用,日誌也沒法用了,死是很正常的。
內存泄漏或者內存耗盡:新的連接無法創建,free的內存比較少。發生內存泄漏的程序很多,但是要想產生一定的後果,就需要這個進程是無限循環的,是個服務進程。當然,內核也是無限循環的,所以,如果內核發生了內存泄漏,情況就更加不妙。內存泄漏是一種很難定位和跟蹤的錯誤,目前還沒看到有什麼好用的工具(當然,用戶空間有一些工具,有靜態分析的,也會動態分析的,但是找內核的內存泄漏,沒有好的開源工具)
內存泄漏和對象的引用計數有很大的關係,再加上c/c 都沒有自動的垃圾回收機制,如果沒有手動釋放內存,問題就會出現。如果要避免這個問題,還是要從代碼上入手,良好的編碼習慣和規範,是避免錯誤的不二法門。
一般我們常說的內存泄漏是指堆內存的泄漏。
堆內存是指程序從堆中分配的,大小任意的(內存塊的大小可以在程序運行期決定),使用完后必須顯示釋放的內存。
應用程序一般使用malloc,realloc,new等函數從堆中分配到一塊內存,使用完后,程序必須負責相應的調用free或delete釋放該內存塊,否則,這塊內存就不能被再次使用,我們就說這塊內存泄漏了。

檢測工具


1.ccmalloc-Linux和Solaris下對C和C 程序的簡單的使用內存泄漏和malloc調試庫。
2.Dmalloc-DebugMallocLibrary.
3.ElectricFence-Linux分發版中由BrucePerens編寫的malloc()調試庫。
4.Leaky-Linux下檢測內存泄漏的程序。
5.LeakTracer-Linux、Solaris和HP-UX下跟蹤和分析C 程序中的內存泄漏。
6.MEMWATCH-由JohanLindh編寫,是一個開放源代碼C語言內存錯誤檢測工具,主要是通過gcc的precessor來進行。
7.Valgrind-DebuggingandprofilingLinuxprograms,aimingatprogramswritteninCandC .
8.KCachegrind-AvisualizationtoolfortheprofilingdatageneratedbyCachegrindandCalltree.
9.IBMRationalPurifyPlus-幫助開發人員查明C/C 、託管.NET、Java和VB6代碼中的性能和可靠性錯誤。PurifyPlus將內存錯誤和泄漏檢測、應用程序性能描述、代碼覆蓋分析等功能組合在一個單一、完整的工具包中。
10.ParasoftInsure -針對C/C 應用的運行時錯誤自動檢測工具,它能夠自動監測C/C 程序,發現其中存在著的內存破壞、內存泄漏、指針錯誤和I/O等錯誤。並通過使用一系列獨特的技術(SCI技術和變異測試等),徹底的檢查和測試我們的代碼,精確定位錯誤的準確位置並給出詳細的診斷信息。能作為MicrosoftVisualC 的一個插件運行。
11.CompuwareDevPartnerforVisualC BoundsCheckerSuite-為C開發者設計的運行錯誤檢測和調試工具軟體。作為MicrosoftVisualStudio和C 6.0的一個插件運行。
12.ElectricSoftwareGlowCode-包括內存泄漏檢查,codeprofiler,函數調用跟蹤等功能。給C 和.Net開發者提供完整的錯誤診斷,和運行時性能分析工具包。
13.CompuwareDevPartnerJavaEdition-包含Java內存檢測,代碼覆蓋率測試,代碼性能測試,線程死鎖,分散式應用等幾大功能模塊。
14.QuestJProbe-分析Java的內存泄漏。
15.ej-technologiesJProfiler-一個全功能的Java剖析工具,專用於分析J2SE和J2EE應用程序。它把CPU、執行緒和內存的剖析組合在一個強大的應用中。
16.BEAJRockit-用來診斷Java內存泄漏並指出根本原因,專門針對Intel平台並得到優化,能在Intel硬體上獲得最高的性能。