网王同人夏日之空下载:C# 内存泄露

来源:百度文库 编辑:偶看新闻 时间:2024/04/29 04:55:34

C#有GC机制,内存泄露可能性比较小,但并不代表没有。事实上当你不恰当处理非托管对象资源时,很容易引起内存泄露,因为非托管对象不受GC控制,得由程序员自己手动释放占用的资源。再者,事实上GC释放内存的时间不是即时的,这些也很有可能引起内存的泄露。最后(并不表示没有),当你错误的使用代码时,也可能引起内存泄露,例如:

TcpListener    listener=new    TcpListener(m_Port);   
  listener.Start();   
  Thread    ListenerThread=new    Thread(new    ThreadStart(AccecptRequest));   
  ListenerThread.Start();   


  private    void    AccecptRequest()   
  {   
         while(true)   
       {   
                   if(listener.Pending())   
                     {   

  }   
                   else   
                   {     
                                 Thread.Sleep(100);   
                     }   
       }   
  }   

listener.Pending()这句代码就会造成内存泄漏,不信你试试看?

 

常见起因:
[1]使用静态引用
[2]未退订的事件-作者认为这是最常见的内存泄漏原因
[3]未退订的静态事件
[4]未调用Dispose方法
[5]使用不彻底的Dispose方法
[6]在Windows Forms中对BindingSource的误用
[7]未在WorkItem/CAB上调用Remove

详见:
How to detect and avoid memory and resources leaks in .NET applications 

http://msdn.microsoft.com/en-us/library/ee658248.aspx
http://msdn.microsoft.com/en-us/library/ee658248.aspx


首先先阐明这篇随笔的意图,只在告诉读者,内存泄露的神不知鬼不觉,希望能引起大家的注意。
一段代码的意思如何正确表达,才能不造成内存泄露呢?很多朋友经常泄露了内存但却查找不到原因。当然在CLI/C++中利用托管对象堆上的垃圾收集器是可以更好地避免这一点。但是在更早的版本中,程序员有必要去手动删除这些相关资源。否则将在程序关闭的时候出现一些错误。
MFC
现在我们去重载一个虚函数virtualvoidDeleteContents();用来在销毁文档数据前调用框架删除一些文档类的数据,(MSDN:Called by the framework to delete the document's data without destroying the CDocument object itself.)
先批评一段代码:
1 void  CGraphicDoc::DeleteContents()
2 {
3      for ( int  i = 0 ;i < m_obArray.GetSize();i ++ )
4      {
5         ……
6     }
7     CDocument::DeleteContents();
8 }
评价:这段代码看似简练,但是却很浪费资源,在第3行的for循环中,i

优化:
 1void CGraphicDoc::DeleteContents()
 2{
 3    int nCount;
 4    nCount=m_obArray.GetSize();
 5    for(int i=0;i 6    {
 7        ……
 8    }
 9    CDocument::DeleteContents();
10}

填写for循环内的语句。这里的任务:删除之前利用CObArray : m_obArray对象保存的一个指针所指向的对象,以及指针本身。因此(以下提供几种常见的错误代码)
代码A:
 1void CGraphicDoc::DeleteContents()
 2{
 3    int nCount;
 4    nCount=m_obArray.GetSize();
 5    for(int i=0;i 6    {
 7        delete m_obArray.GetAt(i);   //删除对象指针所指向的对象
 8        m_obArray.RemoveAt(i);   //删除对应的指针本身
 9    }
10    CDocument::DeleteContents();
11}
代码B:
 1void CGraphicDoc::DeleteContents()
 2{
 3    int nCount;
 4    nCount=m_obArray.GetSize();
 5    for(int i=0;i 6    {
 7        delete m_obArray.GetAt(i);
 8        m_obArray.RemoveAt(0);
 9    }
10    CDocument::DeleteContents();
11}
代码A看起来似乎很符合常规思维,因此也很容易迷惑人。但是在程序运行的时候出现了错误。但是在MSDN中查找CObArray Class Members,查看RemoveAt,其中remarks中有这样一句:In the process, it shifts down all the elements above the removed element(s). It decrements the upper bound of the array but does not free memory. 它的意思就是假设你一共有3个数据 p[0]=a、p[1]=b、p[2]=c,当你删除第0个数据之后,数据将整体向前移动,变为p[0]=b、p[1]=c。这样当你i=nCount-1的时候就根本没有这样的数让你删除,因为假设有那个时刻的话,数据的元素只有1个,而他的编号是0而不是nCount.因此出现了无法删除的现象。因此也就隐含了问题了。
因此有人突发奇想:如果我每次只删除第0个数据的话,那么是否就可以了呢?于是代码B诞生了。可是问题终究没能得到解决。因为假设有一组数据一共3个。删除了编号0的元素(delete语句),移除了该元素的指针,此时i=1,进入删除,又到了delete语句,这时候删除元素i=1这样的语句,这时实际上是删除了先前元素中的第二个元素,而不是第一个。而0与2中间的第1个元素则未被删除。又出现了隐含问题。其实只要将两个都改为0,每次都删除第一个就可以了。
 1void CGraphicDoc::DeleteContents()
 2{
 3    int nCount;
 4    nCount=m_obArray.GetSize();
 5    for(int i=0;i 6    {
 7        delete m_obArray.GetAt(0);
 8        m_obArray.RemoveAt(0);
 9    }
10    CDocument::DeleteContents();
11}
另外可以将程序倒写过来,避免RemoveAt对其进行重新整合队列做产生的不可预料的麻烦。
 1void CGraphicDoc::DeleteContents()
 2{
 3    int nCount;
 4    nCount=m_obArray.GetSize();
 5    while(nCount--)
 6    {
 7        delete m_obArray.GetAt(nCount);
 8        m_obArray.RemoveAt(nCount);
 9    }
10    CDocument::DeleteContents();
11}
12
或者仔细察看MSDN中还有一个函数叫RemoveAll()它是用来删除整个CObArray集合对象的。
 1void CGraphicDoc::DeleteContents()
 2{
 3    int nCount;
 4    nCount=m_obArray.GetSize();
 5    for(int i=0;i 6    {
 7        delete m_obArray.GetAt(i);
 8    }
 9    m_obArray.RemoveAll();
10    CDocument::DeleteContents();
11}

如何创建强命名程序集(Strong Name Assembly)

创建一个强命名程序集首先需要获得一个用强命名实用工具
(Strong Name Utility,即SN.exe,.NET SDK自带)产生的密钥。
下面简要介绍一下SN.exe的一些用法。要产生一个公钥/私钥对:

   a)SN –k MyCompany.Keys
该命名告诉SN.exe创建一个名为MyCompany.keys的文件。MyCompany.keys文件将包含以对以二进制格式存储的公有密钥和私有密钥。

   b)查看公有密钥:
首先生成一个只包含公有密钥的文件: SN –p
   MyCompany.keys MyCompany.PublicKey
然后用-tp参数查看:SN –tp MyCompany.PublicKeys
   Public key is

   00240000048000009400000006020000002400005253413

   10004000001000100bb7214723ffc13901343df4b9c464ebf

   7ef4312b0ae4d31db04a99673e8163768cc0a2a7062e731d

   beb83b869f0509bf8009e90db5c8728e840e782d2cf928dae

   35c2578ec55f0d11665a30b37f8636c08789976d8ee9fe9a5

   c4a0435f0821738e51d6bdd6e6711a5acb620018658cce93

   df37d7e85f9a0104a5845053995ce8

   Public key token is 2dc940d5439468c2

创建好了公钥/私钥对,创建强命名程序集就很容易了。只需要把System.Reflection.AssemblyKeyFileAttribute特性加入到源代码中就可以了:?[assembly:AssemblyKeyFile("MyCompany.keys")]

说明:公钥/私钥对文件的扩展名可以是任意的(也可以没有),因为编译的时候都是以元数据的格式读取的。

   4.程序集的部署方式
一个程序集有两种部署方式:
   a)私有方式
和应用程序部署在同一目录下的程序集称作私有部署程序集。弱命名程序集只能进行私有部署。

   b)全局方式
全局部署方式将程序集部署在一些CLR已确知的地方,当CLR搜索程序集时,它会知道到这些地方去找。强命名程序集既可以进行私有部署,也可以进行全局部署。

   5.如何部署强命名程序集(Strong Name Assembly)和GAC
   a)GAC的概念
如果一个Assembly要被多个应用程序访问,那么他就必须放在一个CLR已确知的目录下,并且CLR在探测到有对该Assembly的引用时,它必须能自动到该目录下寻找这个程序集。这个已确知的目录称作GAC(Global Assembly Cache),就是全局程序集缓存。它一般位于下面的目录下::\Windows\Assembly\GAC。
   GAC的作用就是提供给CLR一个已知的确定的目录去寻找引用的程序集。

   b)GAC的内部结构
   GAC是一个特殊的结构化的目录,用Windows Explorer浏览你会以为它只是一个包含很多程序集的普通目录。其实不是这样的,在命令行下查看,你会发现它实际上包含很多子目录,子目录的名字和程序集的名称是相同的,但它们都不是实际的程序集,实际的程序集位于程序集名对应的目录下。比如进入GCFWK子目录,我们会发现其中又有很多的子目录。机器内每一个安装到GAC的GCFWK.dll在GCFWK中都会有一个子目录。


这里只有一个目录表明只有一个版本的GCFWK程序集被安装。实际的程序集保存在每一个对应的版本目录下。目录的名称以下划线的形式分割为“(Version)_(Culture)_(PublicKeyToken)”。

   GCFWK的语言文化信息为netture,就表示为0.0.0__bf5779af662fc055”。表示得意义是: “GCFWK, Version=1.0.0.0, Culture=neutral,PublicKeyToken=bf5779af662fc055” 如果语言文化信息为”ja”,就表示”1.0.0.0_ja_bf5779af662fc055”

表示得意义是: “GCFWK, Version=1.0.0.0, Culture=ja, PublicKeyToken=bf5779af662fc055”

   c)部署强命名程序集到GAC

   GAC包含很多子目录,这些子目录是用一种算法来产生的,我们最好不要手动将程序集拷贝到GAC中,相反,我们应使用工具来完成这样的工作。因为这些工具知道GAC的内部结构J

在开发和测试中,最常用的工具就是GACUtil.exe。在GAC中注册程序集跟COM注册差不多,但相对更容易:
   1.把程序集添加到GAC中: GACUtil /i sample.dll (参数/i是安装的意思)
   2.把程序集移出GAC GACUtil /u sample.dll (参数/u就移除的意思)
注意:不能将一个弱命名程序集安装到GAC中。
如果你试图把弱命名程序集加入到GAC中,会收到错误信息:”
   Failure adding assembly to the cache: Attempt to install an assembly without a strong name”
   d)强命名程序集的私有部署

把程序集安装到GAC有几个好处。首先,GAC使得很多程序可以共享程序集,这从整体上减少了使用的物理内存;其次,我们很容易将一个新版的程序集部署到 GAC中,并通过一种发布者策略(差不多就是一种重定向方法,比如将原来引用版本为1.0.0.0程序集的程序,通过更改它的配置文件,转而让程序去引用版本为2.0.0.0的程序集)来使用新版本;最后,GAC还提供了对不同版本程序集的并存(side-by-side)管理方式。但是,GAC的安全策略通常只允许管理员更改,同时,向GAC中安装程序集也破坏了.NET框架的简单拷贝部署的许诺。

除了向GAC或者以私有部署方式部署强命名程序集之外,我们还可以将强命名程序集部署在仅为一小部分程序知道的某个任意目录下。配置每一个应用程序的 XML配置文件,让它们指向一个公有目录,这样,在运行时,CLR将知道到哪里去找这个强命名程序集。但这样又有可能会引发”DLL Hell”的问题,因为没有哪个程序可以控制这个程序集何时被卸载。这在.NET中也是不被鼓励的。强命名策略:

生成公钥与私钥对,并对私钥做严格的保护
生成:sn -k keyfile.snk 公钥与私钥对
抽取公钥:sn -p keyfile.snk public.snk 从keyfile中抽取公钥保存到public.snk文件中,以对程序集进行迟签名
跳过验证:sn -Vr assembly 对assembly在开发用机上进行跳过验证处理,该assembly不是已签名的
签名:sn -r assembly keyfile.snk 对assembly 进行迟签名,这一步是在发布之前做
取消跳过验证:sn -Vu assembly 对assembly在开发用机上取消跳过验证,该assembly这时是已签名的
取消所有:sn -Vx 取消所有验证

开发阶段
assembly不是已签名的,但是强命名的strong named,因此需要在开发机上做跳过验证处理

发布阶段
由私钥控制者对assembly进行签名,并在开发机上由开发人员自行取消跳过验证http://leonardleonard.iteye.com/blog/276417