Rex

——生命不止,奋斗不息。
posts - 27, comments - 8, trackbacks - 0, articles - 0
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

关于内存泄露

Posted on 2006-11-22 14:13 W.R 阅读(526) 评论(0)  编辑  收藏 所属分类: 支持技术
一、C++
         一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。看如下一段Ccode:
void GetMemory2(char **p, int num) 

    
*= (char *)malloc(num); 

void Test(void) 

    char 
*str = NULL; 
    GetMemory(
&str, 100); 
    strcpy(str, 
"hello"); 
    printf(str); 

执行Test后,能够输出hello ;但是确实是内存泄露了,该free一下,另外我想C++中free is better than delete!
         以下这段小程序演示了堆内存发生泄漏的情形:
void MyFunction(int nSize)
{
       
char*  p= new char[nSize];
       
if!GetStringFrom( p, nSize ) ){
              MessageBox(“Error”);
              
return;
       }

       …
//using the string pointed by p;
       delete p;
}


         广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。

GDI Object的泄漏是一种常见的资源泄漏:

 

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的要小很多。
         有一个很简单的办法来检查一个程序是否有内存泄漏.就是是用Windows的任务管理器(Task Manager).  运行程序,然后在任务管理器里面查看 “内存使用”和”虚拟内存大小”两项,当程序请求了它所需要的内存之后,如果虚拟内存还是持续的增长的话,就说明了这个程序有内存泄漏问题. 当然如果内存泄漏的数目非常的小,用这种方法可能要过很长时间才能看的出来.
         已经有许多技术被研究出来以应对这个问题,比如Smart Pointer,Garbage Collection等。Smart Pointer技术比较成熟,STL中已经包含支持Smart Pointer的class,但是它的使用似乎并不广泛,而且它也不能解决所有的问题;Garbage Collection技术在Java中已经比较成熟,但是在c/c++领域的发展并不顺畅,虽然很早就有人思考在C++中也加入GC的支持。现实世界就是这样的,作为一个c/c++程序员,内存泄漏是你心中永远的痛。

以发生的方式来分类,内存泄漏可以分为4类:

      1.常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回True,那么pOldBmp指向的HBITMAP对象总是发生泄漏。

      2.偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回True,那么pOldBmp指向的HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

      3.一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。另一个例子:

 

char* g_lpszFileName = NULL;
void SetFileName( const char* lpcszFileName )
{
    
if( g_lpszFileName ){
        free( g_lpszFileName );
    }

    g_lpszFileName 
= strdup( lpcszFileName );
}

如果程序在结束的时候没有释放g_lpszFileName指向的字符串,那么,即使多次调用SetFileName(),总会有一块内存,而且仅有一块内存发生泄漏。

      4.隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。举一个例子:

 

class Connection
{
public:
       Connection( SOCKET s);
       
~Connection();
       …
private:
       SOCKET _socket;
       …
}
;
class ConnectionManager
{
public:
       ConnectionManager()
{

       }

       
~ConnectionManager(){
          list
<Connection>::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
<Connection*> _connlist;
}
;

 假设在Client从Server端断开后,Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。

二、JAVA中的内存泄露
         JAVA有GC自动回收内存,内存泄露是指系统中存在无法回收的内存,有时候会造成内存不足或系统崩溃。在C/C++中分配了内存不释放的情况就是内存泄露。虽然Java存在内存泄露,但是基本上不用很关心它,特别是那些对代码本身就不讲究的就更不要去关心这个了。 Java中的内存泄露当然是指:存在无用但是垃圾回收器无法回收的对象。而且即使有内存泄露问题存在,也不一定会表现出来。看下面的例子:

public class Stack {
 
private Object[] elements=new Object[10];
 
private int size = 0
 
public void push(Object e){
  ensureCapacity();
  elements[size
++= e; 
 }

 
public Object pop(){
  
if( size == 0
   
throw new EmptyStackException(); 
   
return elements[--size];
 }

  
private void ensureCapacity(){
   
if(elements.length == size){
    Object[] oldElements 
= elements;
    elements 
= new Object[2 * elements.length+1];
    System.arraycopy(oldElements,
0, elements, 0, size);
   }

  }

}

假如堆栈加了10个元素,然后全部弹出来,虽然堆栈是空的,没有我们要的东西,但是这是个对象是无法回收的,这个才符合了内存泄露的两个条件:无用,无法回收。再看这个例子:
public class Bad{
 public static Stack s
=Stack();
  static{
   s.push(new Object());
   s.pop(); 
//这里有一个对象发生内存泄露
   s.push(new Object()); 
//上面的对象可以被回收了,等于是自愈了
  }
因为是static,就一直存在到程序退出,但是我们也可以看到它有自愈功能,就是说如果你的Stack最多有100个对象,那么最多也就只有100个对象无法被回收其实这个应该很容易理解,Stack内部持有100个引用,最坏的情况就是他们都是无用的,因为我们一旦放新的进取,以前的引用自然消失!for example:
public class NotTooBad{
 public void doSomething(){
  Stack s
=new Stack();
  s.push(new Object());
  
//other code
  s.pop();
//这里同样导致对象无法回收,内存泄露.
 }
//退出方法,s自动无效,s可以被回收,Stack内部的引用自然没了,所以
 
//这里也可以自愈,而且可以说这个方法不存在内存泄露问题,不过是晚一点
 
//交给GC而已,因为它是封闭的,对外不开放,可以说上面的代码99.9999%
 
//情况是不会造成任何影响的,当然你写这样的代码不会有什么坏的影响,但是
 
//绝对可以说是垃圾代码!没有矛盾吧,我在里面加一个空的for循环也不会有
 
//什么太大的影响吧,你会这么做吗?
}
上面两个例子都不过是小打小闹,但是C/C++中的内存泄露就不是Bad了,而是Worst了。他们如果一处没有回收就永远无法回收,频繁的调用这个方法内存不就用光了!因为Java还有自愈功能(我自己起的名字,还没申请专利),所以Java的内存泄露问题几乎可以忽略了,但是知道的人就不要犯了。
  不知者无罪!Java存在内存泄露,但是也不要夸大其辞。如果你对Java都不是很熟,你根本就不用关心这个,我说过你无意中写出内存泄露的例子就像你中一千万一样概率小,开玩笑了,其实应该是小的多的多! 
         在某些时候,因为代码上写的有问题,会导致某些内存想回收都收不回来,比如下面的代码
Temp1 = new BYTE[100];
Temp2 
= new BYTE[100];
Temp2 
= Temp1;
这样,Temp2的内存地址就丢掉了,而且永远都找不回了,这个时候Temp2的内存空间想回收都没有办法.