c/c++服務器程序內存泄露問題分析及解決

由 www.169it.com 蒐集整理html

對於一個c/c++程序員來講,內存泄漏是一個常見的也是使人頭疼的問題。已經有許多技術被研究出來以應對這個問題,好比 Smart Pointer,Garbage Collection等。Smart Pointer技術比較成熟,STL中已經包含支持Smart Pointer的class,可是它的使用彷佛並不普遍,並且它也不能解決全部的問題;Garbage Collection技術在Java中已經比較成熟,可是在c/c++領域的發展並不暢,雖然很早就有人思考在C++中也加入GC的支持。現實世界就是這樣的,做爲一個c/c++程序員,內存泄漏是你心中永遠的痛。不過好在如今有許多工具可以幫助咱們驗證內存泄漏的存在,找出發生問題的代碼。c++

1.內存泄漏的定義程序員

   通常咱們常說的內存泄漏是指堆內存的泄漏。堆內存是指程序從堆中分配的,大小任意的(內存塊的大小能夠在程序運行期決定),使用完後必須顯示釋放的內存。應用程序通常使用malloc,realloc,new等函數從堆中分配到一塊內存,使用完後,程序必須負責相應的調用free或delete釋放該 內存塊,不然,這塊內存就不能被再次使用,咱們就說這塊內存泄漏了。如下這段小程序演示了堆內存發生泄漏的情形:算法

1
2
3
4
5
6
7
8
9
10
void  MyFunction( int  nSize)
{
  char * p=  new  char [nSize];
  if ( !GetStringFrom( p, nSize ) ){
  MessageBox(「Error」);
   return ;
 }
 … //using the string pointed by p;
  delete  p;
}

當函數GetStringFrom()返回零的時候,指針p指向的內存就不會被釋放。這是一種常見的發生內存泄漏的情形。程序在入口處分配內存,在出口處釋放內存,可是c函數能夠在任何地方退出,因此一旦有某個出口處沒有釋放應該釋放的內存,就會發生內存泄漏。小程序

  廣義的說,內存泄漏不只僅包含堆內存的泄漏,還包含系統資源的泄漏(resource leak),好比核心態HANDLE,GDI Object,SOCKET, Interface等,從根本上說這些由操做系統分配的對象也消耗內存,若是這些對象發生泄漏最終也會致使內存的泄漏。並且,某些對象消耗的是核心態內存,這些對象嚴重泄漏時會致使整個操做系統不穩定。因此相比之下,系統資源的泄漏比堆內存的泄漏更爲嚴重。數組

  GDI Object的泄漏是一種常見的資源泄漏:服務器

1
2
3
4
5
6
7
8
9
10
11
12
13
void  CMyView::OnPaint( CDC* pDC )
{
 CBitmap bmp;
 CBitmap* pOldBmp;
 bmp.LoadBitmap(IDB_MYBMP);
 pOldBmp = pDC->SelectObject( &bmp );
 …
  if ( Something() ){
   return ;
 }
 pDC->SelectObject( pOldBmp );
  return ;
}

當函數Something()返回非零的時候,程序在退出前沒有把pOldBmp選回pDC中,這會致使pOldBmp指向的HBITMAP對象發生泄 漏。這個程序若是長時間的運行,可能會致使整個系統花屏。這種問題在Win9x下比較容易暴露出來,由於Win9x的GDI堆比Win2k或NT的要小很 多。socket

  內存泄漏的發生方式:函數

  以發生的方式來分類,內存泄漏能夠分爲4類:工具

  1) 常發性內存泄漏。發生內存泄漏的代碼會被屢次執行到,每次被執行的時候都會致使一塊內存泄漏。好比例二,若是Something()函數一直返回True,那麼pOldBmp指向的HBITMAP對象老是發生泄漏。

   2) 偶發性內存泄漏。發生內存泄漏的代碼只有在某些特定環境或操做過程下才會發生。好比例二,若是Something()函數只有在特定環境下才返回 True,那麼pOldBmp指向的HBITMAP對象並不老是發生泄漏。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。因此 測試環境和測試方法對檢測內存泄漏相當重要。

  3) 一次性內存泄漏。發生內存泄漏的代碼只會被執行一次,或者因爲算法上的缺陷,致使總會有一塊僅且一塊內存發生泄漏。好比,在類的構造函數中分配內存,在析 構函數中卻沒有釋放該內存,可是由於這個類是一個Singleton,因此內存泄漏只會發生一次。另外一個例子:

1
2
3
4
5
6
7
8
char * g_lpszFileName = NULL;
void  SetFileName(  const  char * lpcszFileName )
{
  if ( g_lpszFileName ){
   free ( g_lpszFileName );
 }
 g_lpszFileName = strdup( lpcszFileName );
} 

若是程序在結束的時候沒有釋放g_lpszFileName指向的字符串,那麼,即便屢次調用SetFileName(),總會有一塊內存,並且僅有一塊內存發生泄漏。

   4)隱式內存泄漏。程序在運行過程當中不停的分配內存,可是直到結束的時候才釋放內存。嚴格的說這裏並無發生內存泄漏,由於最終程序釋放了全部申請的內存。但 是對於一個服務器程序,須要運行幾天,幾周甚至幾個月,不及時釋放內存也可能致使最終耗盡系統的全部內存。因此,咱們稱這類內存泄漏爲隱式內存泄漏。舉一 個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class  Connection
{
  public :
  Connection( SOCKET s);
  ~Connection();
  …
  private :
  SOCKET _socket;
  …
};
class  ConnectionManager
{
  public :
  ConnectionManager(){}
  ~ConnectionManager(){
   list::iterator it;
    for ( it = _connlist.begin(); it != _connlist.end(); ++it ){
     delete  (*it);
   }
   _connlist.clear();
  }
   void  OnClientConnected( SOCKET s ){
   Connection* p =  new  Connection(s);
   _connlist.push_back(p);
  }
   void  OnClientDisconnected( Connection* pconn ){
   _connlist. remove ( pconn );
    delete  pconn;
  }
  private :
  list _connlist;
};

假設在Client從Server端斷開後,Server並無呼叫OnClientDisconnected()函數,那麼表明那次鏈接的 Connection對象就不會被及時的刪除(在Server程序退出的時候,全部Connection對象會在ConnectionManager的析 構函數裏被刪除)。當不斷的有鏈接創建、斷開時隱式內存泄漏就發生了。

  從用戶使用程序的角度來看,內存泄漏自己不會產生什麼危害,做爲通常的用戶,根本感受不到內存泄漏的存在。真正有危害的是內存泄漏的堆積,這會最終消耗盡系統全部的內存。從這個角度來講,一次性內存泄漏並無什麼危 害,由於它不會堆積,而隱式內存泄漏危害性則很是大,由於較之於常發性和偶發性內存泄漏它更難被檢測到。

2.檢測內存泄漏

  檢測內存泄漏的關鍵是要能截獲住對分配內存和釋放內存的函數的調用。截獲住這兩個函數,咱們就能跟蹤每一 塊內存的生命週期,好比,每當成功的分配一塊內存後,就把它的指針加入一個全局的list中;每當釋放一塊內存,再把它的指針從list中刪除。這樣,當 程序結束的時候,list中剩餘的指針就是指向那些沒有被釋放的內存。這裏只是簡單的描述了檢測內存泄漏的基本原理,詳細的算法能夠參見Steve Maguire的<<Writing Solid Code>>。

  若是要檢測堆內存的泄漏,那麼須要截獲住 malloc/realloc/free和new/delete就能夠了(其實new/delete最終也是用malloc/free的,因此只要截獲前 面一組便可)。對於其餘的泄漏,能夠採用相似的方法,截獲住相應的分配和釋放函數。好比,要檢測BSTR的泄漏,就須要截獲 SysAllocString/SysFreeString;要檢測HMENU的泄漏,就須要截獲CreateMenu/ DestroyMenu。(有的資源的分配函數有多個,釋放函數只有一個,好比,SysAllocStringLen也能夠用來分配BSTR,這時就須要 截獲多個分配函數)

  在Windows平臺下,檢測內存泄漏的工具經常使用的通常有三種,MS C-Runtime Library內建的檢測功能;外掛式的檢測工具,諸如,Purify,BoundsChecker等;利用Windows NT自帶的Performance Monitor。這三種工具各有優缺點,MS C-Runtime Library雖然功能上較以外掛式的工具要弱,可是它是免費的;Performance Monitor雖然沒法標示出發生問題的代碼,可是它能檢測出隱式的內存泄漏的存在,這是其餘兩類工具無能爲力的地方。  

  內存泄漏是個大而複雜的問題,即便是Java和.Net這樣有 Gabarge Collection機制的環境,也存在着泄漏的可能,好比隱式內存泄漏。

3.避免內存泄露

  寫服務器程序,最怕的就是內存泄露。由於程序常常運行好幾個月不停,一點點內存泄露都會致使悲劇的發生。常規來講,首要是避免內存泄露,其次是檢查內存泄露。

1)不用new

c++程序,儘可能多用stl,避免用new。我本身寫的代碼,除了在main函數裏面有new外,其餘地方不會再有任何new出現。這樣就把內存管理交給stl去作。

或許你會說,不用new怎麼可能啊?

很簡單,char數組用std::string代替,其餘對象直接拷貝。除非你的對象很大很大,不然,一點點拷貝耗時,徹底能夠忽略不計。

2)每一個重要結構都提供Info函數

給你的每一個重要結構都加上一個Info函數,info函數返回一個string,描述當前結構的狀態,如map的大小,內存佔用的大小。在最頂層,不定時的輸出(或者根據命令輸出)各個對象的info結果。這樣能夠避免隱形的內存泄露,即不是內存泄露,但某個對象保持的大量對象的引用,致使對象沒法被刪除;

即某個對象內部的map,不斷的添加數據,也在不斷的刪除數據,但在某些特殊狀況下,它不會刪除。

3)stl內存泄露的問題

stl幾乎沒有內存泄露,但它有一個內存cache,這個cache對小對象的分配很友好。stl的一個麻煩是,它幾乎不會釋放這些空間,這樣的一個結果是,你看到本身的程序內存佔用不斷的上漲。其實,理論上是不用懼怕的,由於它漲到必定範圍(如,機器只有幾百兆可用空間了),就不會漲了。能夠經過在運行程序前,export GLIBCXX_FORCE_NEW=1,來讓stl不要進行cache。注意,這個僅僅在gcc(g++) 3.3之後有效。

4)valgrind等內存檢測工具

 直接下載,編譯(注意,必須在configure的當前目錄下執行configure,不能另外選一個目錄),安裝。執行:valgrind --num-callers=20 --leak-check=full --leak-resolution=high --show-reachable=yes --log-file=val.log xxx &,等過了幾天後,把它kill,而後慢慢的看val.log文件。

當你採用了前面3個策略後,valgrind幾乎沒有啥效果,反正我歷來沒有從它這裏得到任何有用的信息過。主要是由於前面幾步保證了沒有顯式的內存泄露,因此,valgrind也就找不出來啥內存泄露了。

5)valgrind的工具massif

massif比valgrind好的地方在於,它會告訴你當前內存的分佈狀況。你能夠看到佔用了幾百兆的程序究竟是那些地方佔用了內存。執行:valgrind --tool=massif xxx。通常經過這個均可以看到明顯的內存泄露。這個工具很好,俺用它發現了一個十分異常的狀況,這個狀況是由vector的reserve致使的。原本應該reserve返回數據的個數,結果reserve了命中結果的個數。致使偶爾會出現內存佔用過500M的狀況,但由於沒有內存泄露,因此其餘幾個工具都找不出來,就只有massif提供的堆棧快照能夠發現這個問題。

相關文章
相關標籤/搜索