关于C#中内存管理,托管内存和非托管内存的问题(比较棘手)

Frog1228 2011-12-28 11:19:41
一.我在C#中要调用VC编写的DLL,核心算法都是在VC中写的,里面也涉及到大量的内存的分配和释放。但是在调用DLL里的函数时,有时可以调用通过,有时不能调用通过,我在VC中定位的,出问题的地方都是在分配内存后,进行内存拷贝的地方,如:

double *Image = new double[width*height];
for(i=0; i<width*height; i++)
*(Image+i) = *(Src_Image+i);

我之前一直怕C#这边有内存泄露,导致这样的问题存在,所以我优化过很多次,对于非托管资源Pen,Font,Brush,Graphics,Bitmap(主要是Bitmap图像的释放),在不需要的时候都进行了释放。
就算VC中的DLL函数可以调用通过,如果连续运行几次,在C#这边申请一个比较大的数组比如 double类型,大小为2048*1536*9就会报出“内存不足”的错误,但是物理内存是肯定够用的,我定义了一些工具类用于存放调用DLL计算后得到的一些数据,用数组存储,但是对于数组,在C#中不能手工对其进行释放,我在《C#高级编程》中,看到它用实现IDispose接口和析构函数来加速托管资源和非托管资源的释放,代码如下:

public class Graphics2DShow:IDisposable
{
public virtual void Dispose()
{
Dispose(true);
GC.SuppressFinalize();
}

protected virtual void Dispose(bool disposing)
{
if(!isDisposed)
{
if(disposing)
{
//释放图片非托管资源
}
//释放托管资源
}
isDisposed = true;
}

~Graphics2DShow()
{
Dispose(false);
}
}

这样在代码中调用对象的Dispose方法,就释放托管内存和非托管内存,传递Dispose(bool disposing)的参数表示是用析构函数调用还是IDispose的Dispose()调用,真正实现清理的是该函数,这样做可以保证所有清理代码都放在一个地方。GC.SuppressFinalize();告诉垃圾回收器这个类不再需要调用其析构函数了,因为Dispose()已经完成了所有需要的清理工作,所以析构函数不需要做任何工作了。我的问题就是Dispose(bool disposing)中释放托管资源的地方的代码,对于我的类中含有的数组这些资源,我还是没办法手工进行释放的啊?所以这些数组资源的释放该如何释放呢?

二.对于软件中需要的一些模态窗口,因为使用ShowDialog方法显示的窗口的关闭,并没有对其进行释放,所以我在Form_Closing方法中加了Dispose方法,但是我用AQTime测过,这些模态窗口并没有释放掉,是不是和我将窗口作为参数传给下一个窗口有关?

三.在定义类的字段成员变量后,我喜欢随即初始化,但是这样会存在有时候没用到该变量时,也对其进行了初始化,造成了少量内存的浪费,比如一个引用类型的数组,这在AQTime中也会反映出来,如果在其初次使用的时候对其初始化,我觉得不怎么方便容易出错,还不怎么正规。

反正在这个项目上内存的问题弄得我很头疼,我能用的方法都用出来了,有些问题还是没办法解决。
...全文
2732 28 打赏 收藏 转发到动态 举报
写回复
用AI写文章
28 条回复
切换为时间正序
请发表友善的回复…
发表回复
Frog1228 2012-02-02
  • 打赏
  • 举报
回复
[Quote=引用 23 楼 whossa 的回复:]
缓存池/对象池的概念和语言无关,通常直接的new/delete在性能关键路径上会造成性能损失,多数产品级的服务端程序都会有缓存/对象管理这样的模块,所以你也很少需要直接写new/delete,一般从该模块获取可用对象的一个引用就可,也就是说你可以有多个double[],预先分配好,保存在一定的容器中,需要使用的时候,从该容器获取即可,另外,你可以参考
http://blogs.msdn.com/……
[/Quote]
解决了,年前就解决了,谢谢!

Class BufferPool
{
private int initialPoolSize;
private int bufferSize;

private Queue<double[]> m_FreeBuffers; //pool of buffers

public BufferPool (int initialPoolSize,int bufferSize)
{
this.initialPoolSize = initialPoolSize;
this.bufferSize = bufferSize;
m_FreeBuffers = new Queue<double[]>(initialPoolSize);
for (int i = 0; i < initialPoolSize; i++)
{
m_FreeBuffers.Enqueue(new double[bufferSize]);
}
}

public double[] Checkout() // check out a buffer
{
if (m_FreeBuffers.Count > 0)
{
lock (m_FreeBuffers)
{
if (m_FreeBuffers.Count > 0)
return (double[])m_FreeBuffers.Dequeue();
}
}
//return new double[BUFFER_SIZE]; //instead of creating new buffer,blocking waiting or refusing request may be better
return new double[bufferSize];
}

public void Checkin(double[] buffer) //check in a buffer
{
lock (m_FreeBuffers)
{
m_FreeBuffers.Enqueue(buffer);
}
}
}
}

class ResultUVBufferPool:BufferPool
{

private static ResultUVBufferPool theResultUVBufferPool = null;

public static ResultUVBufferPool getResultUVBufferPool(int initialPoolSize, int bufferSize) //单例模式
{
if(null==theResultUVBufferPool)
{
theResultUVBufferPool = new ResultUVBufferPool(initialPoolSize, bufferSize);
}
return theResultUVBufferPool;
}

private ResultUVBufferPool(int initialPoolSize, int bufferSize): base(initialPoolSize, bufferSize)
{

}
}

对程序中需要用到的数组先开辟出一片内存,需要用的时候,Checkout,用完之后Checkin至缓冲池中,继而做到重复使用这一块内存。缓冲池用单例。
whossa 2012-01-06
  • 打赏
  • 举报
回复
你大可以给该对象添加一个ini方法进行参数方式的初始化,要灵活的话还可以使用配置文件,如何配置这个缓存池对象完全取决于你,关于C#内存管理的资料,最全的是MSDN和CLR开发组各位专家的博客,如果要推荐书籍,可以参考
1. CLR via C#
2. 精通.net互操作
3. .net高级调试

用调试器试试看是最好的, windbg+SOS!
Frog1228 2012-01-05
  • 打赏
  • 举报
回复
[Quote=引用 14 楼 xx_mm 的回复:]
数组是引用类型,在托管堆上分配,你的数组大小至少2048*1536*9*8+同步块索引+内存对象指针+一些额外成员(比如overhead字段)>> 216M, GC第0代 256kb,第1代 2M ,第2代 10M.你的OutOfMemory就正常了!
[/Quote]
有没有写的好的关于C#内存管理方面的书籍和资料啊?我只看过《C#高级编程》中的相应部分。
Frog1228 2012-01-05
  • 打赏
  • 举报
回复
[Quote=引用 23 楼 whossa 的回复:]
缓存池/对象池的概念和语言无关,通常直接的new/delete在性能关键路径上会造成性能损失,多数产品级的服务端程序都会有缓存/对象管理这样的模块,所以你也很少需要直接写new/delete,一般从该模块获取可用对象的一个引用就可,也就是说你可以有多个double[],预先分配好,保存在一定的容器中,需要使用的时候,从该容器获取即可,另外,你可以参考
http://blogs.msdn.com/……
[/Quote]
谢谢!那篇文章我已经看过了。BufferPool定义了单例模式,在初始化时,生成一个512个元素的队列,队列里的每个元素是1024大小的字节数组,Checkin (byte[] buffer)函数应该是往队列中加入一个byte数组的元素,那这个byte数组,不就是从类外进行初始化了吗,这样可以吗?你说的“一般从该模块获取可用对象的一个引用就可,也就是说你可以有多个double[],预先分配好,保存在一定的容器中,需要使用的时候,从该容器获取即可”那我可以直接分配多个216M的数组,保存在一定的容器中?
whossa 2012-01-04
  • 打赏
  • 举报
回复
缓存池/对象池的概念和语言无关,通常直接的new/delete在性能关键路径上会造成性能损失,多数产品级的服务端程序都会有缓存/对象管理这样的模块,所以你也很少需要直接写new/delete,一般从该模块获取可用对象的一个引用就可,也就是说你可以有多个double[],预先分配好,保存在一定的容器中,需要使用的时候,从该容器获取即可,另外,你可以参考
http://blogs.msdn.com/b/yunjin/archive/2004/01/27/63642.aspx
Frog1228 2012-01-04
  • 打赏
  • 举报
回复
[Quote=引用 12 楼 whossa 的回复:]
想明确几个内容,也就是说这个数组是托管代码分配,需要传递给非托管代码,让其填充的是么?
这样的话,你可以参见这个
http://stackoverflow.com/questions/6395305/c-dll-interop-with-c-exposing-multidimensional-arrays-of-unknown-size-and-m

这里介绍如何由托管代码方分配数组提供给……
[/Quote]
不好意思,错了错了,数组是由C#定义分配初始化,传递给非托管代码,让VC端填充的。
Frog1228 2012-01-04
  • 打赏
  • 举报
回复
[Quote=引用 12 楼 whossa 的回复:]
想明确几个内容,也就是说这个数组是托管代码分配,需要传递给非托管代码,让其填充的是么?
这样的话,你可以参见这个
http://stackoverflow.com/questions/6395305/c-dll-interop-with-c-exposing-multidimensional-arrays-of-unknown-size-and-m

这里介绍如何由托管代码方分配数组提供给……
[/Quote]
1.数组其实是由VC端分配,然后C#端调用VC端的DLL导出函数,而数组作为其中一个返回参数,C#端定义一个数组调用该函数。不过你说的“数组由托管代码分配,传递给非托管代码,让它填充”蛮好的。
2.VC端使用malloc/new进行内存分配,这个倒和上面的大数组没有关系,是其它的DLL导出函数,在VC端处理时用到了malloc进行内存分配,而调用这些函数(好几个函数)出错的地方都在他开辟一段内存,进行内存拷贝的地方,也就是类似

double *Image = new double[width*height];
for(i=0; i<width*height; i++)
*(Image+i) = *(Src_Image+i);

的地方(我在VC编的DLL中进行了定位,找到出错的地方)。
3.你说的“使用若干大对象以后(在这里是数组),会使得托管堆内存碎片变多导致后来的对象分配失败,报OOM异常,如果需要大规模的内存分配的互操作,在设计上会采取反复利用该空间,也就是使用缓冲池的方式而不是每次都分配”。缓冲池分配我倒是在C++中看到过,但在C#中不知道能不能用,怎么用。
whossa 2012-01-03
  • 打赏
  • 举报
回复
[Quote=引用 14 楼 xx_mm 的回复:]

数组是引用类型,在托管堆上分配,你的数组大小至少2048*1536*9*8+同步块索引+内存对象指针+一些额外成员(比如overhead字段)>> 216M, GC第0代 256kb,第1代 2M ,第2代 10M.你的OutOfMemory就正常了!
[/Quote]

这个应该是大对象分配的垃圾回收策略受限吧,因为大对象是在第2代才回收,而且不会施行紧缩操作,也就是说相当于一个pinned对象,所以使用若干大对象以后(在这里是数组),会使得托管堆内存碎片变多导致后来的对象分配失败,报OOM异常,如果需要大规模的内存分配的互操作,在设计上会采取反复利用该空间,也就是使用缓冲池的方式而不是每次都分配.另外,你的这个"GC第0代 256kb,第1代 2M ,第2代 10M"是什么意思能解释下吗?
好人吗 2012-01-03
  • 打赏
  • 举报
回复
测试软件运行
*****************************
* 本内容使用CSDN 小秘书回复 *
* 每天回帖即可得10分可用分! *
*****************************
startstartsvip 2012-01-03
  • 打赏
  • 举报
回复
关注一下,这种问题才有看头
蔡袅 2012-01-03
  • 打赏
  • 举报
回复
数组是引用类型,在托管堆上分配,你的数组大小至少2048*1536*9*8+同步块索引+内存对象指针+一些额外成员(比如overhead字段)>> 216M, GC第0代 256kb,第1代 2M ,第2代 10M.你的OutOfMemory就正常了!
whossa 2012-01-03
  • 打赏
  • 举报
回复
另外,你也可以参考这里
http://msdn.microsoft.com/en-us/library/hk9wyw21%28v=VS.71%29.aspx

unmanaged interface
int TestArrayOfInts(int* pArray, int pSize);
managed interface
public static extern int TestArrayOfInts([In, Out] int[] array, int size );

unmanaged interface
int TestRefArrayOfInts(int** ppArray, int* pSize);
managed interface
public static extern int TestRefArrayOfInts( ref IntPtr array,
ref int size );
whossa 2012-01-03
  • 打赏
  • 举报
回复
想明确几个内容,也就是说这个数组是托管代码分配,需要传递给非托管代码,让其填充的是么?
这样的话,你可以参见这个
http://stackoverflow.com/questions/6395305/c-dll-interop-with-c-exposing-multidimensional-arrays-of-unknown-size-and-m

这里介绍如何由托管代码方分配数组提供给非托管代码方让后者填充,然后将其拷贝到托管数组的操作,你要注意的是,托管堆和CRT堆是两回事,没可能让由托管堆分配的内存在CRT堆上释放,反之亦然。所以,如果非托管代码进行内存分配并且使用malloc/new, 它通常也必须同时开放可以释放堆内存的接口(也就是封装了free/delete的操作), 上面有同学说用CoTaskMemAlloc CoTaskMemFree,这个没错儿,但前提是必须该对象是垃圾收集的对象,比如IntPtr就不行,必须手动进行分配和释放。另外,从你贴出来的代码,似乎你们想让dll分配的内存由.net那里释放?
Frog1228 2011-12-29
  • 打赏
  • 举报
回复
这个帖子跑哪去了,找了三页都没找到。。。
sdl2005lyx 2011-12-28
  • 打赏
  • 举报
回复
非托管代码三种内存分配、释放方式:
malloc free
new delete
CoTaskMemAlloc CoTaskMemFree

内存分配、释放必须成对使用,否则也会造成内存泄露!net互操作默认是方式是CoTaskMemAlloc,其垃圾回收自动调用的是CoTaskMemFree,也就是说,如果非托管代码采用COM方式分配内存,可以不用显示释放内存,net帮你搞定!而其他两种,net不支持的方式,必须还是由非托管方来释放!也就是:你要做C++和C#两边都封装一个相应的释放函数!
sdl2005lyx 2011-12-28
  • 打赏
  • 举报
回复
非托管代码三种内存分配、释放方式:
malloc free
new delete
CoTaskMemAlloc CoTaskMemFree

内存分配、释放必须成对使用,否则也会造成内存泄露!net互操作默认是方式是CoTaskMemAlloc,其垃圾回收自动调用的是CoTaskMemFree,也就是说,如果非托管代码采用COM方式分配内存,可以不用显示释放内存,net帮你搞定!而其他两种,net不支持的方式,必须还是由非托管方来释放!也就是:你要做C++和C#两边都封装一个相应的释放函数!
Frog1228 2011-12-28
  • 打赏
  • 举报
回复
[Quote=引用 8 楼 mj_dangerous 的回复:]
在C#这边申请一个比较大的数组比如 double类型,大小为2048*1536*9就会报出“内存不足”的错误
对于楼主的这个错误,我想说一点,你知道你申请了多大的内存空间吗?2048*1536*9*8(8表示每个long真的空间大小)/1024/1024 = 216M字节。
我记得我的老师以前跟我说过,IDE对数组一般默认的是只能非配2M空间。你再瞧瞧吧。如果这样我觉得你的程序有问题了。
[/Quote]
确切地说,是对方经过计算之后得到的数据都保存在这个double数组中,然后传给C#处理,参数是double *一个数组指针,在C#里调用就用ref double匹配。
Frog1228 2011-12-28
  • 打赏
  • 举报
回复
[Quote=引用 8 楼 mj_dangerous 的回复:]
在C#这边申请一个比较大的数组比如 double类型,大小为2048*1536*9就会报出“内存不足”的错误
对于楼主的这个错误,我想说一点,你知道你申请了多大的内存空间吗?2048*1536*9*8(8表示每个long真的空间大小)/1024/1024 = 216M字节。
我记得我的老师以前跟我说过,IDE对数组一般默认的是只能非配2M空间。你再瞧瞧吧。如果这样我觉得你的程序有问题了。
[/Quote]
是216M,对方的VC里是需要这么大小的数组。
MJ_dangerous 2011-12-28
  • 打赏
  • 举报
回复
在C#这边申请一个比较大的数组比如 double类型,大小为2048*1536*9就会报出“内存不足”的错误
对于楼主的这个错误,我想说一点,你知道你申请了多大的内存空间吗?2048*1536*9*8(8表示每个long真的空间大小)/1024/1024 = 216M字节。
我记得我的老师以前跟我说过,IDE对数组一般默认的是只能非配2M空间。你再瞧瞧吧。如果这样我觉得你的程序有问题了。
苦苦的潜行者 2011-12-28
  • 打赏
  • 举报
回复
那么你只需要做的就是VC封装在dll的代码中涉及到垃圾回收的都要在dll中手动回收,
调用到C#后,用CLR管理回收,

最重要的是把你的dll封装好,包括一切可能的内存垃圾都要进行判断和手动回收.
//当然过多的手动回收也可能导致程序出现不可预见的错误.
加载更多回复(4)

110,538

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧