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: https://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:
https://msdn2.microsoft.com/en-us/library/ms228362.aspx
https://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
https://support.microsoft.com/kb/305823/en-us
Marshal.FinalReleaseComObject Method
Marshal.ReleaseComObject
E artigo explicando sobre RCW:
Runtime Callable Wrapper
https://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!