Resposta ao Desafio da Semana #12 [Memory Leak/Crash/Hang - Liberando COM e String null & vazia em C# e VB.NET]

Por: Roberto Alexis Farah

Olá pessoal!

Eis a resposta do desafio #12: http://blogs.technet.com/latam/archive/2006/11/03/desafio-da-semana-12.aspx

PROBLEMA 1

Observe os diferentes cenários com um string válida, vazia e nula.

Execução com uma string válida.

string str = "Abcd";

                            

DoSomething(str);

    

Eis a thread no lado gerenciado (.NET), especificamente no CLR (Common Language Runtime):

ESP EIP

0012f438 00cc011e Demo.Program.DoSomething(System.String)

    PARAMETERS:

        str = 0x01271be0 ß Objeto string.

    LOCALS:

        0x0012f43c = 0x00000000

        0x0012f438 = 0x00000000

      0x0012f450 = 0x00000000

        0x0012f44c = 0x00000000

        0x0012f448 = 0x00000000

        0x0012f444 = 0x00000000

ESP/REG Object Name

0012f47c 00cc0096 Demo.Program.Main(System.String[])

    PARAMETERS:

        args = 0x01271bd0

    LOCALS:

   <CLR reg> = 0x01271be0

ESP/REG Object Name

0012f69c 79e88f63 [GCFrame: 0012f69c]

Eis o detalhe do objeto acima:

Name: System.String

MethodTable: 790fa3e0

EEClass: 790fa340

Size: 26(0x1a) bytes

GC Generation: 0

 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

String: Abcd

Fields:

      MT Field Offset Type VT Attr Value Name

790fed1c 4000096 4 System.Int32 0 instance 5 m_arrayLength

790fed1c 4000097 8 System.Int32 0 instance 4 m_stringLength ß É maior que 0.

790fbefc 4000098 c System.Char 0 instance 41 m_firstChar

790fa3e0 4000099 10 System.String 0 shared static Empty

    >> Domain:Value 0014c1b8:790d6584 <<

79124670 400009a 14 System.Char[] 0 shared static WhitespaceChars

    >> Domain:Value 0014c1b8:01271754 <<

Excução com uma string vazia:

DoSomething("Def");

                            

str = "";

OS Thread Id: 0x72b8 (0)

ESP EIP

ESP/REG Object Name

eax 01271c14 System.String

ecx 01271c14 System.String

esi 01271c14 System.String

0012f478 00cc00d8 Demo.Program.DoSomething(System.String)

    PARAMETERS:

        str = 0x01271c14 ß String vazia mas objeto string é válido. Aponta para 790fa3e0 que é o MethodTable.

    LOCALS:

        <no data>

        <no data>

        <no data>

        <no data>

        <no data>

        <no data>

ESP/REG Object Name

eax 01271c14 System.String

ecx 01271c14 System.String

esi 01271c14 System.String

0012f47c 00cc00b4 Demo.Program.Main(System.String[])

    PARAMETERS:

        args = 0x01271bd0

    LOCALS:

        <CLR reg> = 0x01271c14

ESP/REG Object Name

eax 01271c14 System.String

ecx 01271c14 System.String

esi 01271c14 System.String

0012f47c 01271bd0 System.Object[] (System.String[])

0012f534 01271bd0 System.Object[] (System.String[])

0012f69c 79e88f63 [GCFrame: 0012f69c]

Eis a string vazia:

Name: System.String

MethodTable: 790fa3e0

EEClass: 790fa340

Size: 18(0x12) bytes

GC Generation: 0

 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)

String:

Fields:

      MT Field Offset Type VT Attr Value Name

790fed1c 4000096 4 System.Int32 0 instance 1 m_arrayLength

790fed1c 4000097 8 System.Int32 0 instance 0 m_stringLength ß Tamanho 0.

790fbefc 4000098 c System.Char 0 instance 0 m_firstChar

790fa3e0 4000099 10 System.String 0 shared static Empty

    >> Domain:Value 0014c1b8:790d6584 <<

79124670 400009a 14 System.Char[] 0 shared static WhitespaceChars

    >> Domain:Value 0014c1b8:01271754 <<

Agora a execução com um objeto string que é null:

str = null;

DoSomething(str);

Vamos aos detalhes:

OS Thread Id: 0x72b8 (0)

ESP EIP

ESP/REG Object Name

0012f478 00cc00d8 Demo.Program.DoSomething(System.String)

    PARAMETERS:

        str = 0x00000000 ß É como se fosse um ponteiro em C++ apontando para NULL.

    LOCALS:

        <no data>

        <no data>

        <no data>

        <no data>

        <no data>

        <no data>

ESP/REG Object Name

0012f47c 00cc00bf Demo.Program.Main(System.String[])

    PARAMETERS:

        args = 0x01271bd0

    LOCALS:

        <CLR reg> = 0x00000000

ESP/REG Object Name

0012f47c 01271bd0 System.Object[] (System.String[])

0012f534 01271bd0 System.Object[] (System.String[])

0012f69c 79e88f63 [GCFrame: 0012f69c]

                            

Em .NET todo objeto deriva de System.Object e um objeto como uma string pode ser vazia mas não nula, ou pode ser nula. São conceitos diferentes.

Observe os objetos gerenciados na pilha:

OS Thread Id: 0x72b8 (0)

ESP/REG Object Name

0012f47c 01271bd0 System.Object[] (System.String[]) ß Eis nosso objeto: 01271bd0

0012f534 01271bd0 System.Object[] (System.String[])

0012f6e0 01271bd0 System.Object[] (System.String[])

0012f708 01271bd0 System.Object[] (System.String[])

Nosso objeto:

Name: System.Object[]

MethodTable: 79124228

EEClass: 7912479c

Size: 16(0x10) bytes

GC Generation: 0

Array: Rank 1, Number of elements 0, Type CLASS

Element Type: System.String ß Tipo string… mas é apenas um objeto nulo. Não foi inicializado com uma string ou string vazia.

Fields:

None

Agora a execução vai para:

// Checa se string é vazia.

if(0 == str.Length)

E temos no lado de código nativo:

eax=00000000 ebx=0012f4ac ecx=00000000 edx=00000000 esi=00000000 edi=00000000

eip=00cc0123 esp=0012f438 ebp=0012f474 iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010246

Demo!Demo.Program.DoSomething(System.String)+0x4b:

00cc0123 8b7808 mov edi,dword ptr [eax+8] ds:0023:00000008=???????? ß Interrogação é memória não inicializada, afinal, o endereço 0x8 é inválido.

0x8 nada mais é que eax que é 0x0 + offset que é 0x8. Porque o offset? Porque o offset é onde se está armazenado o tamanho da string em um objeto do tipo System.String.

Eis a pilha no lado nativo:

ChildEBP RetAddr

0012f474 00cc00bf Demo!Demo.Program.DoSomething(System.String)+0x4b

0012f490 79e88f63 Demo!Demo.Program.Main(System.String[])+0x4f

0012f490 79e88ee4 mscorwks!CallDescrWorker+0x33

0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3

0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c

0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20

0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call_RetArgSlot+0x18

0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220

0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6

0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398

0012ff68 79f05f61 mscorwks!ExecuteEXE+0x59

0012ffb0 79011b5f mscorwks!_CorExeMain+0x11b

0012ffc0 7c816fd7 mscoree!_CorExeMain+0x2c

0012fff0 00000000 KERNEL32!BaseProcessStart+0x23

E a exceção:

ExceptionAddress: 00cc0123 (Demo!Demo.Program.DoSomething(System.String)+0x0000004b)

   ExceptionCode: c0000005 (Access violation)

  ExceptionFlags: 00000000

NumberParameters: 2

   Parameter[0]: 00000000

   Parameter[1]: 00000008

Attempt to read from address 00000008

O depurador pega a exceção. Nesse ponto continuei a execução para forçar uma exceção fatal. (2nd chance exception):

(72bc.72b8): CLR notification exception - code e0444143 (first chance)

(72bc.72b8): Access violation - code c0000005 (!!! second chance !!!)

eax=00000000 ebx=0012f4ac ecx=00000000 edx=00000000 esi=00000000 edi=00000000

eip=00cc0123 esp=0012f438 ebp=0012f474 iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

Demo!Demo.Program.DoSomething(System.String)+0x4b:

00cc0123 8b7808 mov edi,dword ptr [eax+8] ds:0023:00000008=????????

E no lado .NET temos:

System.NullReferenceException was unhandled

  Message="Object reference not set to an instance of an object."

  Source="Demo"

  StackTrace:

       at Demo.Program.DoSomething(String str) in C:\Development\My Tools\BLOG Articles\Article #17\Demo\Demo\Program.cs:line 31

       at Demo.Program.Main(String[] args) in C:\Development\My Tools\BLOG Articles\Article #17\Demo\Demo\Program.cs:line 24

       at System.AppDomain.nExecuteAssembly(Assembly assembly, String[] args)

   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)

       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()

       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)

   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)

       at System.Threading.ThreadHelper.ThreadStart()

Portanto, o primeiro problema é que o código não lida com situações onde a string é nula. O comentário do código explica que a rotina deve retornar false se a string do parâmetro for vazia ou nula mas a implementação apenas testa se a string é vazia!

PROBLEMA 2

Sem dúvida o maior causador de instâncias nunca decrementadas no COM+ e consequentes hangs.

Eis a execução antes de instanciar o componente COM do Word.

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

-----------------------------

Total 0

CCW 0

RCW 0 ß Runtime Callable Wrapper, responsável por gerenciar o contador de referência de objetos COM.

ComClassFactory 0

Free 0

Após a instanciação do componente:

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

-----------------------------

Total 1

CCW 0

RCW 1 ß Ok, internamente houve um AddRef() do COM.

ComClassFactory 0

Free 0

Na linha do msWord.Quit() :

OS Thread Id: 0x72b8 (0)

ESP/REG Object Name

eax 0127244c System.Boolean

esi 0127244c System.Boolean

0012f43c 0127243c Microsoft.Office.Interop.Word.ApplicationClass

0012f440 01272420 System.String ABCD

0012f450 0127244c System.Boolean

0012f460 01271be0 System.String Abcd

0012f46c 01271be0 System.String Abcd

0012f47c 01271bd0 System.Object[] (System.String[])

0012f534 01271bd0 System.Object[] (System.String[])

0012f6e0 01271bd0 System.Object[] (System.String[])

0012f708 01271bd0 System.Object[] (System.String[])

Após finally:

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

    1 001af254 0 0 00000000 none 0127243c Microsoft.Office.Interop.Word.ApplicationClass

-----------------------------

Total 1

CCW 0

RCW 1              ß Ei, Relase() do COM não foi chamado! O objeto COM não sabe que pode sair da memória pois ainda há referências para suas interfaces!

ComClassFactory 0

Free 0

RuntimeCallableWrappers (RCW) a serem liberados:

  RCW CONTEXT THREAD Apartment

       0 80000001 0 MTA

MTA Interfaces to be released: 1

STA Interfaces to be released: 0

Após várias execuções do mesmo método temos um aumento constante das referências para o objeto COM MTA do Word.

Veja a segunda execução logo após a segunda instanciação do componente do Word:

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

    1 001af254 0 0 00000000 none 0127243c Microsoft.Office.Interop.Word.ApplicationClass

    2 001af284 0 0 00000000 none 01272470 Microsoft.Office.Interop.Word.ApplicationClass

-----------------------------

Total 2

CCW 0

RCW 2

ComClassFactory 0

Free 0

Portanto, no segundo problema o wrapper que o .NET usa para se comunicar com o COM não decrementa as referências logo o objeto continua na memória!

SOLUÇÃO – PROBLEMA 1

Para o Problema 1 a solução é bastante simples…

Em .NET Framework 1.1 e 2.0:

static bool DoSomething(string str)

{

          // Checa se string é nula ou vazia.

          if((null == str) || (0 == str.Length))

          {

                   return false;

          }

Em .NET Framework 2.0 especificamente:

static bool DoSomething(string str)

{

          // Checa se string é nula ou vazia.

          if(String.IsNullOrEmpty(str))

          {

                   return false;

          }

Para evitar esse tipo de problema, mantenha em mente que uma string Null é diferente de uma string vazia.

<IMPORTANTE>

String vazia:

Uma string vazia é uma instância de System.String que contém zero caracteres.

String nula (null string):

Uma string nula não referencia uma instância de um objeto System.String e tentativas de se executar métodos de uma string nula resultam em uma exceção do tipo NullReferenceException.

</IMPORTANTE>

Eis alguns artigos de referência:

http://msdn2.microsoft.com/en-us/library/ms228362.aspx

http://msdn2.microsoft.com/en-us/library/sxw2ez55.aspx

SOLUÇÃO – PROBLEMA 2

Para o Problema 2 a solução também é bastante simples.

O que? Você pensou em set msWord = Nothing no VB.NET ou msWord = null no C#?

Não, infelizmente não… isso funciona no mundo nativo, em Visual Basic 6 e ASP por exemplo. Em .NET as coisas são um pouco diferentes...

A solução se baseia em utilizarmos uma chamada de método que força o wrapper usado pelo .NET a liberar a referência para o objeto COM.

Eis o código corrigido e o resultado da depuração do mesmo:

namespace Demo

{

          class Program

          {

                   static void Main(string[] args)

                   {

                             string str = "Abcd";

                            

                             DoSomething(str);

                            

                             DoSomething("Def");

                            

                             str = "";

                            

                             DoSomething(str);

                             str = null;

                             DoSomething(str); // Agora não falha mais!

                            

                   }

                  

                   static bool DoSomething(string str)

                   {

                             // Checa se string é nula ou vazia.

                             if((null == str) || 0 == str.Length)

                             {

                                      return false;

                             }

                            

                             str = str.ToUpper();

                                     

                             Microsoft.Office.Interop.Word.Application msWord = null;

                                                                            

                             try

                             {

                                      msWord = new Microsoft.Office.Interop.Word.Application();

                                      object saveChanges = false;

                                      object missing = null;

                                      msWord.Quit(ref saveChanges, ref missing, ref missing);

                             }

                             catch(OutOfMemoryException ex)

                             {

                                      Console.WriteLine("Não há memória disponível...");

                             }

                             finally

                             {

// Libera wrapper da memória que, por sua vez, libera as referências para o

// objeto COM.

                                      // Evita NullReferenceException caso tentarmos liberar o objeto e

                                      // sem que o mesmo tenha sido inicializado!

                                      if(msWord != null)

                                      {

                                                // Para .NET Framework 2.0.

                                      System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msWord); }

                                     

                                  msWord = null;

                             }

                            

                             return true;

                   }

          }

}

Note que para compatibilidade com .NET Framework 1.1 e 2.0, dentro do finally e dentro do if() acima você deve usar:

while(System.Runtime.InteropServices.Marshal.ReleaseComObject(msWord) > 0);

<IMPORTANTE>

System.Runtime.InteropServices.Marshal.ReleaseComObject() decrementa em um o contador de referência interna portanto se faz necessário chamá-lo em um loop para garantir que o wrapper liberou todas as referências para o objeto COM.

Na aplicação usada no desafio não é necessário se chamar dentro de um loop, entretanto, recomendo que o loop sempre seja utilizado pois numa aplicação real caso haja mudanças que impliquem em incremento de contador de referências interna do COM não é necessário se preocupar em mudar o código que libera o objeto COM, se o loop acima for utilizado. Portanto, eliminando um potencial bug na aplicação! J

</IMPORTANTE>

Eis a depuração logo antes de instanciarmos o componente Word, usando a solução para o problema 2:

Thread 0:

ChildEBP RetAddr

0012f474 00cc0096 Demo!Demo.Program.DoSomething(System.String)+0x8e

0012f490 79e88f63 Demo!Demo.Program.Main(System.String[])+0x26

0012f490 79e88ee4 mscorwks!CallDescrWorker+0x33

0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3

0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c

0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20

0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call_RetArgSlot+0x18

0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220

0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6

0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398

0012ff68 79f05f61 mscorwks!ExecuteEXE+0x59

0012ffb0 79011b5f mscorwks!_CorExeMain+0x11b

0012ffc0 7c816fd7 mscoree!_CorExeMain+0x2c

0012fff0 00000000 KERNEL32!BaseProcessStart+0x23

Referências para RCW:

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

-----------------------------

Total 0

CCW 0

RCW 0

ComClassFactory 0

Free 0

Agora logo após a instanciação:

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

    1 03ba004c 0 0 00000000 none 0127243c Microsoft.Office.Interop.Word.ApplicationClass

-----------------------------

Total 1

CCW 0

RCW 1

ComClassFactory 0

Free 0

RuntimeCallableWrappers (RCW) a serem liberados:

     RCW CONTEXT THREAD Apartment

       0 80000001 0 MTA

MTA Interfaces to be released: 1

STA Interfaces to be released: 0

Agora acabo de executar a chamada para ReleaseComObject() :

Index SyncBlock MonitorHeld Recursion Owning Thread Info SyncBlock Owner

    1 03ba004c 0 0 00000000 none 0127243c Microsoft.Office.Interop.Word.ApplicationClass

-----------------------------

Total 1

CCW 0

RCW 0 ß Liberado! Com isso o objeto COM será liberado também!

ComClassFactory 0

Free 0

RuntimeCallableWrappers (RCW) a serem liberados:

     RCW CONTEXT THREAD Apartment

       0 80000001 0 MTA

MTA Interfaces to be released: 1

STA Interfaces to be released: 0 ß Se fosse um componente VB 6 sendo chamado então seria STA.

Eis a pilha do CLR:

OS Thread Id: 0xa7e4 (0)

ESP EIP

ESP/REG Object Name

esi 0127244c System.Boolean

0012f434 00cc01ee Demo.Program.DoSomething(System.String)

    PARAMETERS:

        str = 0x01272420

    LOCALS:

        0x0012f43c = 0x0127243c ß Equivale ao objeto msWord do código fonte.

        0x0012f450 = 0x0127244c

        0x0012f44c = 0x00000000

        0x0012f438 = 0x00000000

        0x0012f448 = 0x00000000

        0x0012f444 = 0x00000001

ESP/REG Object Name

esi 0127244c System.Boolean

0012f43c 0127243c Microsoft.Office.Interop.Word.ApplicationClass

0012f440 01272420 System.String ABCD

0012f450 0127244c System.Boolean

0012f460 01271be0 System.String Abcd

0012f46c 01271be0 System.String Abcd

0012f47c 00cc0096 Demo.Program.Main(System.String[])

    PARAMETERS:

        args = 0x01271bd0

    LOCALS:

        <CLR reg> = 0x01271be0

ESP/REG Object Name

esi 0127244c System.Boolean

0012f47c 01271bd0 System.Object[] (System.String[])

0012f534 01271bd0 System.Object[] (System.String[])

0012f69c 79e88f63 [GCFrame: 0012f69c]

Eis o objeto RCW:

Name: Microsoft.Office.Interop.Word.ApplicationClass

MethodTable: 03435afc

EEClass: 0336ea90

Size: 16(0x10) bytes

GC Generation: 0

 (C:\WINDOWS\assembly\GAC\Microsoft.Office.Interop.Word\11.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Word.dll)

Fields:

      MT Field Offset Type VT Attr Value Name

790f9c18 4000184 4 System.Object 0 instance 00000000 __identity

790fea70 4000277 8 ...ections.Hashtable 0 instance 00000000 m_ObjectToDataMap ß Wrapper liberado! Agora é com o GC.

Como estou usando uma versão Debug, o código não está otimizado portanto o IL e o código disassemblado são mais fáceis de visualizar. No caso, eis parte do método DoSomething() :

00cc01e8 e833167a78 call mscorlib_ni!System.Runtime.InteropServices.Marshal.ReleaseComObject(System.Object) (79461820)

00cc01ed 90 nop

00cc01ee 33d2 xor edx,edx ß Conteúdo de msWord em null.

00cc01f0 8955c8 mov dword ptr [ebp-38h],edx ß Atribui null a msWord.

00cc01f3 90 nop

00cc01f4 58 pop eax

Agora a execução de msWord = null.

Note que é uma boa prática se atribuir null (ou nothing em Visual Basic .NET) para um objeto, após usá-lo, e sempre fazer comparações com null antes de usá-lo. Isso ajuda a identificar bugs mais facilmente.

edx:

00140608 7c97c500

ebp-0x38 (msWord):

0012f43c 0127243c

Após msWord = null:

edx:

00000000 ????????

ebp-0x38 (msWord):

0012f43c 00000000 ( msWord = null; )

Portanto, nesse ponto seguramente nosso objeto msWord é null e o RCW foi liberado da memória, liberando o objeto COM!

Note que é uma boa prática de programação configurar objetos para null após liberá-los/usá-los e sempre testar se um objeto não é null antes de usar.

Por que isso? Porque se você compara o objeto contra null e tenta utilizá-lo, após ter liberado os recursos associados ao mesmo (no nosso caso o wrapper para o objeto COM), a comparação vai suceder e o código vai lançar uma exceção!

Por exemplo:

if(msWord != null)

{

          // msWord não mais aponta para uma instância do wrapper após a linha abaixo ser executada...

          System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msWord);

}

if(msWord != null) // Ao mesmo tempo o if() retornará true porque não explicitamente configuramos msWord = null;

{

          // Usa método de msWord. Exceção!!! msWord não aponta para uma instância válida do seu tipo.

}

Outro importante ponto é o código dentro de finally. Não podemos assumir no código dentro de finally que o objeto que apontava para null agora aponta para uma instância do tipo, afinal, no bloco try uma exceção OutOfMemoryException poderia ter sido lançada na inicialização do objeto! Se assumirmos que o objeto sempre é inicializado teremos uma exceção NullReferenceException dentro do bloco finally quando o objeto for null!

Eis os artigos de referências:

PRB: COM+ Instance Count Does Not Decrease When Called from .NET Application

http://support.microsoft.com/kb/305823/en-us

 

Marshal.FinalReleaseComObject Method

http://msdn2.microsoft.com/en-us/library/system.runtime.interopservices.marshal.finalreleasecomobject(VS.80).aspx

 

Marshal.ReleaseComObject

http://msdn2.microsoft.com/en-us/library/system.runtime.interopservices.marshal.releasecomobject(VS.80).aspx

 

E artigo explicando sobre RCW:

Runtime Callable Wrapper

http://msdn2.microsoft.com/en-us/library/8bwh56xe.aspx

Hum... agora você está entendendo porque aquele componente no COM+ sendo chamado pela sua página ASPX sempre aumenta o contador de referências, algumas vezes causando hang no servidor. Ótimo!

Até a próxima!