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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容