天天看點

ArcEngine開發中記憶體不能釋放 非托管資源的管理 AE記憶體不能釋放的可能場景 産生問題的原因 釋放釋放COM對象引用方法 測試驗證

Net中将資料分為兩種類型:值資料類型和引用資料類型,這兩種資料類型存儲在記憶體中的不同的地方:值資料類型存儲在堆棧中,而引用類型存儲在記憶體的托管堆中。程式中的變量定義在棧空間中,引用類型的對象實際配置設定在堆記憶體中,當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元件

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對象的使用,一般采用下面的形式:

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

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);

代碼中一般采用如下标準寫法

?

1 2 3 4 5 6 7 8 9 10 11 12

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)都沒有問題。

?

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79

void

DisplayString(string msg)

{

Console.WriteLine(msg);

}

void

localReleaseComObj(object obj)

{

if

(obj ==

null

)

return

;

while

(Marshal.ReleaseComObject(obj) >

) { }

}

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,

);

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 =

;

//int counsts = pFeaCls.FeatureCount(null);//總要素個數

try

{

tmStart = DateTime.Now;

DisplayString(

"開始于:"

+ tmStart.ToLongTimeString()+

"\r\n"

);

tsMax = TimeSpan.MinValue;

int

idxStart =

;

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 =

;

pCur = pFeaCls.Search(

null

,

false

);

numCurHasBuild++;

      

//循環擷取遊标内的要素

pFea = pCur.NextFeature();

while

(pFea !=

null

)

{

string tmp = pFea.get_Value(

).ToString();

pFea = pCur.NextFeature();

pos++;

if

(pos %

5000

==

) 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);

}

繼續閱讀