1. 程式人生 > >c++記憶體洩漏和記憶體碎片的問題

c++記憶體洩漏和記憶體碎片的問題

1.記憶體洩漏的定義

   一般我們常說的記憶體洩漏是指堆記憶體的洩漏。堆記憶體是指程式從堆中分配的,大小任意的(記憶體塊的大小可以在程式執行期決定),使用完後必須顯示釋放的記憶體。應用程式一般使用mallocreallocnew等函式從堆中分配到一塊記憶體,使用完後,程式必須負責相應的呼叫freedelete釋放該 記憶體塊,否則,這塊記憶體就不能被再次使用,我們就說這塊記憶體洩漏了。

對於C和C++這種沒有Garbage Collection 的語言來講,我們主要關注兩種型別的記憶體洩漏:

 堆記憶體洩漏(Heap leak)。對記憶體指的是程式執行中根據需要分配通過malloc,realloc new等從堆中分配的一塊記憶體,再是完成後必須通過呼叫對應的 free或者delete 刪掉。如果程式的設計的錯誤導致這部分記憶體沒有被釋放,那麼此後這塊記憶體將不會被使用,就會產生Heap Leak. 

系統資源洩露(Resource Leak).主要指程式使用系統分配的資源比如 Bitmap,handle ,SOCKET等沒有使用相應的函式釋放掉,導致系統資源的浪費,嚴重可導致系統效能降低,系統執行不穩定。

2、內存洩漏的後果

程式執行後置之不理,並且隨著時間的流失消耗越來越多的記憶體(比如伺服器上的後臺任務,尤其是嵌入式系統中的後臺任務,這些任務可能被執行後很多年內都置之不理);

新的記憶體被頻繁地分配,比如當顯示電腦遊戲或動畫視訊畫面時;

程式能夠請求未被釋放的記憶體(比如共享記憶體),甚至是在程式終止的時候;

洩漏在作業系統內部發生;

洩漏在系統關鍵驅動中發生;

記憶體非常有限,比如在嵌入式系統或便攜裝置中;

當運行於一個終止時記憶體並不自動釋放的作業系統(比如AmigaOS)之上,而且一旦丟失只能通過重啟來恢復。

3、如何發現記憶體洩漏

有些簡單的記憶體洩漏問題可以從在程式碼的檢查階段確定。還有些洩漏比較嚴重的,即在很短的時間內導致程式或系統崩潰,或者系統報告沒有足夠記憶體,也比較容易發現。最困難的就是洩漏比較緩慢,需要觀測幾天、幾周甚至幾個月才能看到明顯異常現象。那麼如何在比較短的時間內檢測出有沒有潛在的記憶體洩漏問題呢?實際上不同的系統都帶有記憶體監視工具,我們可以從監視工具收集一段時間內的堆疊記憶體資訊,觀測增長趨勢,來確定是否有記憶體洩漏。在 Linux 

平臺可以用 ps 命令,來監視記憶體的使用,比如下面的命令 (觀測指定程序的VSZ)

ps -aux

靜態分析,包括手動檢測和靜態工具,是代價最小的方法。

1)當使用 C/C++ 進行開發時,採用良好的一致的程式設計規範是防止記憶體問題第一道也是最重要的措施。檢測是編碼標準的補充。二者各有裨益,但結合使用效果特別好。專業的 或 C++ 專業人員甚至可以瀏覽不熟悉的原始碼,並以極低的成本檢測記憶體問題。通過少量的實踐和適當的文字搜尋,您能夠快速驗證平衡的 *alloc() 和 free() 或者 new 和 delete 的源主體。人工檢視此類內容通常會出現像清單 中一樣的問題,可以定位出在函式 LeakTest 中的堆變數 Logmsg 沒有釋放。

2)程式碼靜態掃描和分析的工具比較多,比如 splint, PC-LINT, BEAM 等。因為 BEAM 支援的平臺比較多,這以 BEAM 為例,做個簡單介紹,其它有類似的處理過程。

BEAM 可以檢測四類問題沒有初始化的變數;廢棄的空指標;記憶體洩漏;冗餘計算。而且支援的平臺比較多。

BEAM 支援以下平臺:

Linux x86 (glibc 2.2.4)

Linux s390/s390x (glibc 2.3.3 or higher)

Linux (PowerPC, USS) (glibc 2.3.2 or higher)

AIX (4.3.2+)

Window2000 以上

或者使用內嵌程式自動檢測

可以過載記憶體分配和釋放函式 new 和 delete,然後編寫程式定期統計記憶體的分配和釋放,從中找出可能的記憶體洩漏。或者呼叫系統函式定期監視程式堆的大小,關鍵要確定堆的增長是洩漏而不是合理的記憶體使用。這類方法比較複雜,在這就不給出詳細例子了。

2、動態執行檢測

實時檢測工具主要有 valgrind, Rational purify 等。

1、 Valgrind

valgrind 是幫助程式設計師尋找程式裡的 bug 和改程序序效能的工具。程式通過 valgrind 執行時,valgrind 收集各種有用的資訊,通過這些資訊可以找到程式中潛在的 bug 和效能瓶頸。

Valgrind 現在提供多個工具,其中最重要的是 MemcheckCachegrindMassif 和 CallgrindValgrind 是在 Linux 系統下開發應用程式時用於除錯記憶體問題的工具。它尤其擅長髮現記憶體管理的問題,它可以檢查程式執行時的記憶體洩漏問題。其中的 memecheck 工具可以用來尋找 cc++ 程式中記憶體管理的錯誤。可以檢查出下列幾種記憶體操作上的錯誤:

讀寫已經釋放的記憶體

讀寫記憶體塊越界(從前或者從後)

使用還未初始化的變數

將無意義的引數傳遞給系統呼叫

記憶體洩漏

 2Rational purify

Rational Purify 主要針對軟體開發過程中難於發現的記憶體錯誤、執行時錯誤。在軟體開發過程中自動地發現錯誤,準確地定位錯誤,提供完備的錯誤資訊,從而減少了除錯時間。同時也是市場上唯一支援多種平臺的類似工具,並且可以和很多主流開發工具整合。Purify 可以檢查應用的每一個模組,甚至可以查出複雜的多執行緒或程序應用中的錯誤。另外不僅可以檢查 C/C++,還可以對 Java 或 .NET 中的記憶體洩漏問題給出報告。

在 Linux 系統中,使用 Purify 需要重新編譯程式。通常的做法是修改 Makefile 中的編譯器變數。下面是用來編譯本文中程式的 Makefile

CC=purify gcc

首先執行 Purify 安裝目錄下的 purifyplus_setup.sh 來設定環境變數,然後執行 make 重新編譯程式。

./purifyplus_setup.sh

下面給出編譯一個程式碼檔案的示例,原始碼檔案命名為 test3.cpp. 用 purify 和 g++ 的編譯命令如下,‘-g’是編譯時加上除錯資訊。

purify g++ -g test3.cpp o test

執行編譯生成的可執行檔案 test,就可以得到圖1,可以定位出記憶體洩漏的具體位置。

./test

4、如何避免記憶體洩漏

其實記憶體洩漏的原因可以概括為:呼叫了malloc/new等記憶體申請的操作,但缺少了對應的free/delete,總之就是,malloc/newfree/delete的數量多。我們在程式設計時需要注意這點,保證每個malloc都有對應的free,每個new都有對應的deleted!!!平時要養成這樣一個好的習慣。

要避免記憶體洩漏可以總結為以下幾點:

1、程式設計師要養成良好習慣,保證malloc/newfree/delete匹配;

2、一遍又一遍的看程式碼,希望能看出記憶體洩露的bug 

3、檢查malloc/newfree/delete是否匹配,一些工具也就是這個原理。要做到這點,就是利用巨集或者鉤子,在使用者程式與執行庫之間加了一層,用於記錄記憶體分配情況。

4.、總結了好些規律:要成對使用,free掉的記憶體指標要置空,在儘量少的函式中申請和釋放記憶體,等等 

5、使用了大量的記憶體村洩露檢測工具,結合各種自動測試軟體,採取瘋狂加變態的測試方法,試圖找出所有可能的記憶體洩露bug。 

  6、程式碼規模超過千萬行,記憶體從一個模組被傳遞到另一個或多個,誰知道傳遞給哪個模組,最後在那裡釋放的了,反正我保證那個指標指向的資料是好的就行了。由於程式碼的規模,窮舉式的測試根本不可能,只有一遍一遍的看程式碼,祈禱自己的程式碼沒有記憶體洩露的問題。 

記憶體碎片

1、什麼是記憶體碎片

    記憶體碎片---描述一個系統中所有的不可用的空閒記憶體;這些資源之所以仍然未被使用,是因為負責分配記憶體的分配器使這些記憶體無法使用。這一問題通常都會發生,原因在於空閒記憶體以小而不連續方式出現在不同的位置。由於分配方法決定記憶體碎片是否是一個問題,因此記憶體分配器在保證空閒資源可用性方面扮演著重要的角色。

2、記憶體碎片產生的原因

    原因在與空閒記憶體以小而不連續的方式出現在不同的位置(記憶體分配較小,並且分配的這些小的記憶體生存週期又較長,反覆申請後將產生記憶體碎片的出現)。記憶體分配程式浪費記憶體的基本方式有三種:即額外開銷、內部碎片以及外部碎片(圖 1)。記憶體分配程式需要儲存一些描述其分配狀態的資料。這些儲存的資訊包括任何一個空閒記憶體塊的位置、大小和所有權,以及其它內部狀態詳情。一般來說,一個執行時間分配程式存放這些額外資訊最好的地方是它管理的記憶體。記憶體分配程式需要遵循一些基本的記憶體分配規則。例如,所有的記憶體分配必須起始於可被 4或 16 整除(視處理器體系結構而定)的地址。記憶體分配程式把僅僅預定大小的記憶體塊分配給客戶,可能還有其它原因。當某個客戶請求一個 43 位元組的記憶體塊時,它可能會獲得 44位元組、48位元組 甚至更多的位元組。由所需大小四捨五入而產生的多餘空間就叫內部碎片。

  外部碎片的產生是當已分配記憶體塊之間出現未被使用的差額時,就會產生外部碎片。例如,一個應用程式分配三個連續的記憶體塊,然後使中間的一個記憶體塊空閒。記憶體分配程式可以重新使用中間記憶體塊供將來進行分配,但不太可能分配的塊正好與全部空閒記憶體一樣大。倘若在執行期間,記憶體分配程式不改變其實現法與四捨五入策略,則額外開銷和內部碎片在整個系統壽命期間保持不變。雖然額外開銷和內部碎片會浪費記憶體,因此是不可取的,但外部碎片才是嵌入系統開發人員真正的敵人,造成系統失效的正是分配問題。



記憶體碎片的產生:

1.動態記憶體分配問題:

       記憶體分配有靜態分配和動態分配兩種
靜態分配在程式編譯連結時分配的大小和使用壽命就已經確定,而應用上要求作業系統可以提供給程序執行時申請和釋放任意大小記憶體的功能,這就是記憶體的動態分配。
        因此動態分配將不可避免會產生記憶體碎片的問題,那麼什麼是記憶體碎片?記憶體碎片即“碎片的記憶體”描述一個系統中所有不可用的空閒記憶體,這些碎片之所以不能被使用,是因為負責動態分配記憶體的分配演算法使得這些空閒的記憶體無法使用,這一問題的發生,原因在於這些空閒記憶體以小且不連續方式出現在不同的位置。因此這個問題的或大或小取決於記憶體管理演算法的實現上。

       為什麼會產生這些小且不連續的空閒記憶體碎片呢?

       實際上這些空閒記憶體碎片存在的方式有兩種:a.內部碎片 b.外部碎片
  內部碎片的產生:因為所有的記憶體分配必須起始於可被 4、8 或 16 整除(視處理器體系結構而定)的地址或者因為MMU的分頁機制的限制,決定記憶體分配演算法僅能把預定大小的記憶體塊分配給客戶。假設當某個客戶請求一個 43 位元組的記憶體塊時,因為沒有適合大小的記憶體,所以它可能會獲得 44位元組、48位元組等稍大一點的位元組,因此由所需大小四捨五入而產生的多餘空間就叫內部碎片。
 外部碎片的產生: 頻繁的分配與回收物理頁面會導致大量的、連續且小的頁面塊夾雜在已分配的頁面中間,就會產生外部碎片假設有一塊一共有100個單位的連續空閒記憶體空間,範圍是0~99。如果你從中申請一塊記憶體,如10個單位,那麼申請出來的記憶體塊就為0~9區間。這時候你繼續申請一塊記憶體,比如說5個單位大,第二塊得到的記憶體塊就應該為10~14區間。如果你把第一塊記憶體塊釋放,然後再申請一塊大於10個單位的記憶體塊,比如說20個單位。因為剛被釋放的記憶體塊不能滿足新的請求,所以只能從15開始分配出20個單位的記憶體塊。現在整個記憶體空間的狀態是0~9空閒,10~14被佔用,15~24被佔用,25~99空閒。其中0~9就是一個記憶體碎片了。如果10~14一直被佔用,而以後申請的空間都大於10個單位,那麼0~9就永遠用不上了,變成外部碎片。

    2.系統記憶體回收機制問題:

      記憶體碎片是一個系統問題,反覆的malloc和 free,而free後的記憶體又不能馬上被系統回收利用。這個與系統對記憶體的回收機制有關。


3、記憶體碎片的弊端與優點

    缺點:

大量的記憶體碎片會使系統緩慢,原因在於虛擬記憶體的使用會使記憶體與硬碟之間的資料交換稱為系統

緩慢的根源,最終造成記憶體的枯竭!

   優點:

 減少記憶體碎片,提高分配速度,便於記憶體管理,防止記憶體洩露

4、如何避免記憶體碎片的產生

   1>少用動態記憶體分配的函式(儘量使用棧空間)

   2>分配記憶體和釋放的記憶體儘量在同一個函式中

3>儘量一次性申請較大的記憶體2的指數次冪大小的記憶體空間,而不要反覆申請小記憶體(少進行記憶體的分割)

   4>使用記憶體池來減少使用堆記憶體引起的記憶體碎片

   5>儘可能少地申請空間。

   6>儘量少使用堆上的記憶體空間~

   7>做記憶體池,也就是自己一次申請一塊足夠大的空間,然後自己來管理,用於大量頻繁地new/delete操作。

   記憶體管理系統將能夠及時合併相鄰空閒記憶體塊,得到更大的空閒記憶體。這樣並不會導致記憶體碎片的出現。即使相鄰空間不空閒,這樣產生的碎片還是比較少的,但是對於遊戲(執行時間較長)或者手機(記憶體較小)