ArcEngine开发中内存不能释放浅析

托管资源内存管理机制

.Net中将数据分为两种类型:值数据类型和引用数据类型,这两种数据类型存储在内存中的不同的地方:值数据类型存储在堆栈中,而引用类型存储在内存的托管堆中。
值类型有:bool ,byte ,char ,decimal ,double ,enum ,float ,int ,long ,sbyte ,short ,struct ,uint ,ulong ,ushort,都来自于System.TypeValue。
引用类型:class, interface, delegate,object,string所有的引用类型都继承自System.Object。
程序中的变量定义在栈空间中,引用类型的对象实际分配在堆内存中,当CLR发现堆上的数据不再被栈引用时,CLR的垃圾回收器就会自动清理他们,当然也可以手动清理,调用GC.Collect() 即可,一般只有在处理大数据的数据回收时才调用,马上释放内存,程序变量在内存中是以栈的形式来填充的,如果内存中间有了一部分内存不再使用了,会造成内存的不连续,会导致程序资源和相应时间的浪费,还好垃圾回收器还做了一个工作-将那些还在使用的数据移动到堆的顶端,让他们再次是连续的,及更改对象的地址,从而腾出连续的内存空白空间,提供了性能。
对于托管堆中的数据是单独有一块区域(大对象堆 >85,000个字节)用来存放大数据,这样做的好处是因为数据的移动比较消耗性能,垃圾回收器为提供性能,不对这类数据移动。
在GC中有一个计数器,他会在一个对象创建时,将其纳入计数器,并计数为0。当某处使用该对象时,计数+1,当某处该对象使用完毕,或置为null,则计数-1。(计数数值永远保持大于等于0.)当GC开启回收并发现一个对象的计数为0时,会将其清理掉,释放对应内存。需要说明垃圾回收器不保证在回收一次的情况就能把所有不再引用的数据清除。(垃圾回收相对于引用计数为0的时刻,会有延迟)
托管资源由于是由.Net机制完全控制监管的,他的计数会严格且有效,所以能够及时清理。

非托管资源的管理

非托管资源指非.Net机制托管对象,如图像对象,数据库连接,文件句柄.网络连接等等。
在.net的类中,主要有以下各类:OleDBDataReader,StreamWriter,ApplicationContext,Brush,Component,ComponentDesigner,Container,Context,Cursor,FileStream,Font,Icon,mage,Matrix,Object,OdbcDataReader,OleDBDataReader,Pen,Regex,Socket,StreamWriter,Timer,Tooltip 等
非托管资源对象GC并不知道如何处理,这就需要程序员处理非托管资源的清空方式。
实现方式有两种:一种是实现IDisposable接口的DIspose方法;实现析构方法;两者的区别在于,当一个类实现IDisposable接口后,在使用该类时通过主动调用Dispose方法,另一种是使用using语句使用该类(using语句执行完毕会执行Dispose方法,即便出现中间异常),皆可及时清理非托管资源。

AE内存不能释放的可能场合

由于AO底层基于COM架构,ESRI系列产品基本都直接AO组件,对于.NET组件只是通过RCW对COM组件实现了一次封装。AO组件特点如下:
1.原生的组件属于非托管组件,这可以从产品的进化过程得出结论。
2.目前的托管组件例如AE .net开发包,都是直接通过RCW(runtime callable wrapper)方式调用AO底层的组件
3.Desktop依然是直接基于COM,通过CCW(COM Callable Wrapper)方式支持我们用.net写的一些组件(如command,tool等)
4.托管和非托管的比较,非托管的COM组件自己控制组件的生存周期,托管组件由CLR(Common Language Runtime)来管理,即通过GC(Garbage ollection)机制自动回收。
由上述的第四个特点,托管组件自己不能控制生存期,CLR释放不及时,经常抛出各种COM错误,如果有循环操作,错误出现的频率非常高。
对于.NET+AE开发,有时候出会出现文件对象不能操作(如删除,重复打开的问题),这类问题的原因一般是由于资料被锁住了,前面一个进程在对其打开后操作时忘记了对其进行释放导致的。由于.net采用的自动垃圾回收,不用开发人员对托管资源进行内存回收,但是当操作的对象是外部资源(非托管资源)时候,就容易出现问题,对于需要手动释放资源的场合,大概有以下几种情形:
1.当一个COM对象包含系统资源(比如文件句柄,数据库连接扥等,AE中对MDB,SDE等数据库操作时),特别是数据库连接,如果用一个连接对数据库对象进行了操作之后,没有即时释放资源,其它的用户就不能对该对象进行操作,你需要显式释放COM对象,以释放其持有的资源。在AE中就是工作空间对象IWorkspace
2.如果COM对象不含有系统资源,但使用量大,比如new 5万个Geometry,使用显式释放COM对象,可以节省内存,如果不显示释放的话,可能会造成内存空间不足的问题。
3.尽可能的顺手显式释放所有的RCW对象,以节省资源。
4.在使用ArcEngine中的游标对象时,一定要在使用完之后进行对象的释放,具体就是ICursors,IEnums;而且需要使用marshal.releasecomobject方法来进行对象的释放,赋值为null只是使引用计算减少了1,有时候并不能达到目的。
5.可认为RCW包装的COM对象本身是一个非托管资源,RCW的finalizer可以在垃圾回收时释放这个资源,你也可以调用ReleaseComObject释放这个资源,就像调用Dispose一样。而COM对象本身包含的系统资源,RCW并不能正确识别,并在finalizer中做特别处理,所以需要显式释放。

产生问题的原因

首先对比一下COM Object与.Net Object
1.COM Object的客户必须自己管理COM Object的lifetime;.Net Object由CLR来管理(GC)
2.COM Ojbect的客户通过调用Query Interface查询COM Object是否支持某接口并得到接口指针;.Net Object的客户使用Reflection得到Object的Description.Property和Method.
3.COM Object是通过指针引用,并且object在内存中的位置是不变的;.Net对象则可以在GC进行收集时通过Compact Heap来改变Object的位置。
为了实现COM与.Net的交互,.Net使用Wrapper技术提供了RCW(Runtime Callable Wrapper)和CCW(COM Callable Wrapper)。.Net对象调用COM对象的方法时CLR就会创建一个RCW对象;COM对象调用.Net对象的方法时就会创建一个CCW 对象。

Net调用COM组件

一.Net调用COM组件
RCW的主要作用:
1.RCW是Runtime生成的一个.Net类,它包装了COM组件的方法,并内部实现了对COM组件的调用
2.Marshal between .Net object and COM object.Marshal方法的参数和返回值等。如C#的string和COM的BSTR之间的转换
3.CLR为每个COM对象创建一个RCW,每个COM对象只有一个RCW对象
4.RCW包含COM对象的接口指针,管理COM对象的引用计数。RCW自身的释放由GC管理。

COM对象的内存管理

1.COM对象不在托管堆里创建,也不能被GC搜索并收集。COM对象使用引用计数机制释放内存。
2.RCW作为COM对象的包装器,包含了COM对象的接口指针,并且为这个接口指针进行引用计数。RCW本身作为.Net对象是由GC管理并收集。当RCW被收集后,它的finalizer就会释放接口指针并销毁COM对象。
3.将对象引用设为null,如app = null,只会使该引用指向的对象的引用计数减少1,并不会使其立刻产生垃圾回收,因此需要将该对象的所有接口的引用全部设为null之后(一个对象有多个接口,一个接口会产生多个引用),才可以通过GC.Collect()产生垃圾回收。我们要把代码中所有引用到COM对象(wbs,wb等等)的变量设置为null,来消除对RCW的引用,从而在方法内部就可以让GC收集到RCW,进而释放掉COM对象。
4.由于GC收集时间的不确定性(由于COM对象是RCW的Finalizer执行后释放,因此即使RCW被收集了,执行Finalizer还要在另外一个线程上排队进行),这将导致COM对象在RCW被收集前滞留在内存。如果这个COM对象占用内存较大或者资源数有限(FileHandle, DBConnection),这就有可能引发内存泄漏或者程序异常。
对于RCW对象的使用,一般采用下面的形式:

            private void ExecuteTransfer()
            {
             ApplicationClass app;
             try
             {
              app = new ApplicationClass();
              WorkBooks wbs = app.Workbooks;
              WorkBook wb = wbs.Add(XlWBATemplate.xlWBATWorksheet);
              ...
              //Use app to generate Excel Object
             }
             catch
             {}
             finally
             {
              app.Quit();
              app = null;
              //消除所有对RCW的引用,否则GC.Collect()无法收集到RCW
              wbs = null;
              wb = null;
              //....其他引用到COM对象的变量设置为NULL
             }
             GC.Collect();//有效
            }

释放释放COM对象引用方法

可以采用COMReleaser或者Marshal.ReleaseComObject
COMReleaser是对Marshal.ReleaseComObject对象的封装,确保所有对对象的引用都得到释放。
ReleaseComObject只是减少对RCW的引用,调用一次就减少1,直到减少到0的时候,就会触发垃圾回收,释放RCW所指向的COM对象的内存资源。
int System.Runtime.InteropServices.Marshal.ReleaseComObject(object o)可
调用这个方法后,RCW就会释放object接口指针,它就是一个空的Wrapper,它与COM对象的联系就断了,再对其进行调用就会Runtime Error.这时候,如果没有其他变量对对象进行引用,垃圾回收器就会在下次垃圾回收的时候将其内存进行清理。
在程序编制过程中要注意以下几点:
1.尽量不要用多线程操作,我们的产品本身不支持多线程,多线程是一个陷阱,虽然.net构建线程非常方便,但是一旦采用多线程问题将会无穷尽,而且多是不能调试的错误。
2.AO的.net开发包中的对象释放方法
ESRI.ArcGIS.ADF.COMSupport.AOUninitialize.Shutdown() ;必须的,一般放在窗口关闭的Dispose函数中。
ESRI.ArcGIS.ADF.ComReleaser.ReleaseCOMObject(comObject); 用了他就不要用CLR中的释放函数
3..NET Framework 下的释放方法
.NET Framework 1.1下的释放方法
System.Runtime.InteropServices.Marshal.ReleaseComObject(comObject);
一般写成
while (Marshal.ReleaseComObject(comObject) > 0){}
.NET Framework 2.0下的释放方法
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(comObject);
代码中一般采用如下标准写法

    private void NAR(object o)
    {
        try
        {
            System.Runtime.InteropServices.Marshal.ReleaseComObject(o);
        }
        catch { }
        finally
        {
            o = null;
        }
    }

1.1下的方法和2.0下的方法有不同,我们的组件最好还是用1.1的ReleaseComObject方法释放,2.0下的有时还会有异常抛出。.net中的com对象只手动释放一次即可。 特别需要注意new出来的对象

测试验证

所有代码在同一个函数中执行

这时候是否释放内存都没有问题,因为函数执行完就自动释放内存空间了。对象的引用只有一个变量,所以不存在这个问题。
是否正确进行Marshal.ReleaseComObject(obj)都没有问题。

        void DisplayString(string msg)
        {
            Console.WriteLine(msg);
        }
        void localReleaseComObj(object obj)
        {
            if (obj == null) return;
            while (Marshal.ReleaseComObject(obj) > 0) { }
        }
        private void TestShape(string Path, string name)
        {
            IAoInitialize aoinitialize = new AoInitializeClass();
            aoinitialize.Initialize(esriLicenseProductCode.esriLicenseProductCodeEngine);
            string ssName = "controlpoint";//图层名称(里面有4w多条数据)
         //测试两种释放方式的执行时间
            DateTime tmStart = DateTime.Now;
            DisplayString("开始于:"+tmStart.ToLongTimeString()+"\r\n");
            DateTime tmEnd;
            DateTime tmMiddle1;
            DateTime tmMiddle2;
            TimeSpan ts, tsMax;
          //具体执行代码
             IWorkspaceFactory pWSF = new ShapefileWorkspaceFactoryClass();
             IFeatureWorkspace pWS = (IFeatureWorkspace)pWSF.OpenFromFile(Path, 0);
            IFeatureWorkspace pFeaWs = pWS as IFeatureWorkspace;//我已经建立好的SDE工作空间对象
            IFeatureClass pFeaCls = pFeaWs.OpenFeatureClass(name);
            string strWhere = string.Format("stationserieseventid in ('f9bd16ed-ae2a-454c-9eba-7123dc41af28','7e3d0d4a-8c5e-49b5-8977-e060cd4cef6d','a89300a5-3503-4976-b5d2-3d5a712f7b36')");
            IFeatureCursor pCur = null;
            IFeature pFea = null;
            IQueryFilter pFilter = new QueryFilterClass();
            int numCurHasBuild = 0;
            //int counsts = pFeaCls.FeatureCount(null);//总要素个数
            try
            {
                tmStart = DateTime.Now;
                DisplayString("开始于:" + tmStart.ToLongTimeString()+"\r\n");
                tsMax = TimeSpan.MinValue;
                int idxStart = 0;
                int idxEnd = 92160;
                int idxTmpNode = idxStart + 2000;
                for (int idx = idxStart; idx < 3; idx++)
                {
                    tmMiddle1 = DateTime.Now;
                    strWhere = "objectid = '" + idx.ToString() + "'";
                    pFilter.WhereClause = strWhere;
               //获取游标对象
                   // pCur = pFeaCls.Search(pFilter, false);//如果游标对象没有释放,那么一次循环不能超过280,否则会爆‘超出打开游标最大数’错误
                    long pos = 0;
                    pCur = pFeaCls.Search(null, false);
                    numCurHasBuild++;
               //循环获取游标内的要素
                    pFea = pCur.NextFeature();
                    while (pFea != null)
                    {
                        string tmp = pFea.get_Value(0).ToString();
                        pFea = pCur.NextFeature();
                        pos++;
                        if (pos % 5000 == 0) DisplayString("正在循环:" + pos.ToString () + "\r\n");
                    }
                    pCur = null;//像这样,对象实际上是没有释放的;依旧会在283条的时候报错
                    //Marshal.ReleaseComObject(pCur);//这种方式可以完全释放掉对象,此时可以完全循环完4w条数据
                    //localReleaseComObj(pCur);//自己写的一个方法,达到释放游标pCur的目的
                    if (pCur != null)
                        localReleaseComObj(pCur);
                    //tmMiddle2 = DateTime.Now;
                    //ts = tmMiddle2 - tmMiddle1;
                    //if (ts > tsMax)
                    //    tsMax = ts;
                }
                tmEnd = DateTime.Now;
              //  DisplayString("循环中耗时最多的一次时间为:"+tsMax.TotalSeconds+"\r\n");
                DisplayString("执行完一轮循环;消耗的总时间为:"+(tmEnd-tmStart).TotalSeconds+"\r\n");
            }
            catch (Exception ex)
            {
                DisplayString("在第" + numCurHasBuild.ToString() + "处发生错误!\r\n" + ex.Message);
                throw new Exception(ex.Message);
            }
        }

将部分代码抽离出来放在函数中执行

通过测试也发现没啥问题

参考资料

http://www.cnblogs.com/qingtian-jlj/p/5971386.html
https://www.add-in-express.com/creating-addins-blog/2013/11/05/release-excel-com-objects/
http://www.cnblogs.com/daidaigua/archive/2012/04/20/2459243.html
http://www.cnblogs.com/mcwind/archive/2007/10/31/943944.html

你可能感兴趣的:(ArcEngine开发中内存不能释放浅析)