Preliminares
Considere o seguinte código:
A assinatura do método
Marshal.FinalReleaseComObject é a seguinte:
public static int FinalReleaseComObject(Object o)
Criamos um objeto COM simples, fazemos algum trabalho e o liberamos imediatamente. Parece que o que poderia dar errado? Sim, criar um objeto dentro de um loop infinito não é uma boa prática, mas o
GC fará todo o trabalho sujo. A realidade é um pouco diferente:

Para entender por que a memória vaza, você precisa entender como a
dinâmica funciona. Já existem vários artigos sobre esse assunto em Habré, por exemplo,
este , mas eles não entram em detalhes de implementação, portanto, conduziremos nossa própria pesquisa.
Primeiro, examinaremos em detalhes o mecanismo de trabalho
dinâmico , reduziremos o conhecimento adquirido em uma única imagem e, no final, discutiremos as razões desse vazamento e como evitá-lo. Antes de mergulhar no código, vamos esclarecer os dados de origem: qual combinação de fatores leva ao vazamento?
Os experimentos
Talvez criar muitos objetos
COM nativos seja uma má idéia em si? Vamos verificar:
Tudo está bem desta vez:

Vamos voltar à versão original do código, mas altere o tipo de objeto:
E, novamente, sem surpresas:

Vamos tentar a terceira opção:
Bem, agora, definitivamente devemos ter o mesmo comportamento! Hein? Não :(

Uma imagem semelhante será se você declarar com como um
objeto ou se trabalhar com o
COM gerenciado . Resuma os resultados experimentais:
- Instanciar objetos COM nativos por si só não leva a vazamentos - o GC lida com êxito com a limpeza de memória
- Ao trabalhar com qualquer classe gerenciada , não ocorrem vazamentos
- Ao converter explicitamente um objeto para outro, tudo está bem também
Olhando para o futuro, podemos adicionar o fato de que trabalhar com objetos
dinâmicos (chamar métodos ou trabalhar com propriedades) por si só também não causa vazamentos. A conclusão sugere-se: um vazamento de memória ocorre quando passamos um objeto
dinâmico (sem conversão "manual") contendo
COM nativo , como parâmetro de método.
Precisamos ir mais fundo
É hora de lembrar o
que é essa
dinâmica :
Referência rápidaO C # 4.0 fornece um novo tipo de dinâmica . Esse tipo evita a verificação de tipo estático pelo compilador. Na maioria dos casos, funciona como um tipo de objeto . Em tempo de compilação, supõe-se que um elemento declarado como dinâmico suporte qualquer operação. Isso significa que você não precisa pensar sobre a origem do objeto - a partir da API COM, uma linguagem dinâmica como o IronPython, usando reflexão ou de outro lugar. Além disso, se o código for inválido, serão gerados erros em tempo de execução.
Por exemplo, se o método exampleMethod1 no código a seguir tiver exatamente um parâmetro, o compilador reconhecerá que a primeira chamada para o método ec.exampleMethod1 (10, 4) é inválida porque contém dois parâmetros. Isso resultará em um erro de compilação. A segunda chamada de método, dynamic_ec.exampleMethod1 (10, 4) não é verificada pelo compilador, pois dynamic_ec é declarado como dinâmico , portanto. não haverá erros de compilação. No entanto, o erro não passará despercebido para sempre - será detectado em tempo de execução.
static void Main(string[] args) { ExampleClass ec = new ExampleClass();
class ExampleClass { public ExampleClass() { } public ExampleClass(int v) { } public void exampleMethod1(int i) { } public void exampleMethod2(string str) { } }
O código que usa variáveis
dinâmicas sofre alterações significativas durante a compilação. Este código:
dynamic com = Activator.CreateInstance(comType); Marshal.FinalReleaseComObject(com);
Transforma-se no seguinte:
object instance = Activator.CreateInstance(typeFromClsid);
Onde
o__0 é a classe estática gerada e
p__0 é o campo estático nela:
private class o__0 { public static CallSite<Action<CallSite, Type, object>> p__0; }
Nota: para cada interação com dinâmica , um campo CallSite é criado. Isso, como veremos mais adiante, é necessário para otimizar o desempenhoObserve que nenhuma menção à
dinâmica é deixada - nosso objeto agora está armazenado em uma variável do tipo
objeto . Vamos percorrer o código gerado. Primeiro, é criada uma ligação, que descreve o que e o que estamos fazendo:
Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "FinalReleaseComObject", (IEnumerable<Type>) null, typeof (Foo), (IEnumerable<CSharpArgumentInfo>) new CSharpArgumentInfo[2] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, (string) null) })
Esta é uma descrição da nossa operação dinâmica. Deixe-me lembrá-lo de que passamos uma variável
dinâmica para o método
FinalReleaseComObject .
- CSharpBinderFlags.ResultDiscarded - o resultado da execução do método não é usado no futuro
- "FinalReleaseComObject" - o nome do método chamado
- typeof (Foo) - contexto de operação; o tipo de chamada
CSharpArgumentInfo - descrição dos parâmetros de ligação. No nosso caso:
- CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.UseCompileTimeType | CSharpArgumentInfoFlags.IsStaticType, (string) null) - descrição do primeiro parâmetro - a classe Marshal: é estática e seu tipo deve ser considerado durante a ligação
- CSharpArgumentInfo.Create (CSharpArgumentInfoFlags.None, (string) null) - descrição do parâmetro do método, normalmente não há informações adicionais.
Se não se tratava de chamar um método, mas de, por exemplo, chamar uma propriedade de um objeto
dinâmico , haveria apenas um
CSharpArgumentInfo que descreve o próprio objeto
dinâmico .
CallSite é um invólucro sobre uma expressão dinâmica. Ele contém dois campos importantes para nós:
- Atualização pública T
- público T Target
A partir do código gerado, fica claro que, quando qualquer operação é executada, o
Target é chamado com parâmetros que a descrevem:
Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, typeof (Marshal), instance);
Em conjunto com o
CSharpArgumentInfo descrito acima
, esse código significa o seguinte: você precisa chamar o método FinalReleaseComObject na classe estática Marshal com o parâmetro de instância No momento da primeira chamada, o mesmo delegado é armazenado no
Target e no
Update . O delegado da
atualização é responsável por duas tarefas importantes:
- Vincular uma operação dinâmica a estática (o próprio mecanismo de licitação está além do escopo deste artigo)
- Formação de cache
Estamos interessados no segundo ponto. Deve-se notar aqui que, ao trabalhar com um objeto dinâmico, precisamos verificar a validade da operação a cada vez. Essa é uma tarefa que consome muitos recursos, então quero armazenar em cache os resultados dessas verificações. No que diz respeito à chamada de um método com um parâmetro, precisamos lembrar o seguinte:
- O tipo no qual o método é chamado
- O tipo de objeto transmitido pelo parâmetro (para garantir que ele possa ser convertido para o tipo do parâmetro)
- A operação é válida
Então, ao chamar
Target novamente, não precisamos executar ligações relativamente caras: basta comparar os tipos e, se corresponderem, chamar a função objetivo. Para resolver esse problema, um
ExpressionTree é criado para cada operação dinâmica, que armazena as
restrições e a
função objetivo à qual a expressão dinâmica foi vinculada.
Esta função pode ser de dois tipos:
- Erro de ligação : por exemplo, um método é chamado em um objeto dinâmico que não existe ou um objeto dinâmico não pode ser convertido no tipo de parâmetro para o qual é passado: é necessário lançar uma exceção como Microsoft.CSharp.RuntimeBinderException: 'NoSuchMember'
- O desafio é legal: basta executar a ação necessária
Essa
ExpressionTree é formada durante a execução
do delegado
Update e armazenada no
Target .
Alvo - cache
L0 , falaremos mais sobre o cache mais tarde.
Portanto, o
Target armazena o último
ExpressionTree gerado pelo delegado
Update . Vamos ver como essa
regra se parece com um exemplo de um tipo
gerenciado passado para o método
Boo :
public class Foo { public void Test() { var type = typeof(int); dynamic instance = Activator.CreateInstance(type); Boo(instance); } public void Boo(object o) { } }
.Lambda CallSite.Target<System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]>( Actionsss.CallSite $$site, ConsoleApp12.Foo $$arg0, System.Object $$arg1) { .Block() { .If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32) { .Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) } } } .Else { .Default(System.Void) }; .Block() { .Constant<Actionsss.Ast.Expression>(IIF((($arg0 TypeEqual Foo) AndAlso ($arg1 TypeEqual Int32)), returnUnamedLabel_0 ({ ... }) , default(Void))); .Label .LabelTarget CallSiteBinder.UpdateLabel: }; .Label .If ( .Call Actionsss.CallSiteOps.SetNotMatched($$site) ) { .Default(System.Void) } .Else { .Invoke (((Actionsss.CallSite`1[System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]])$$site).Update)( $$site, $$arg0, $$arg1) } .LabelTarget #Label1: } }
O bloco mais importante para nós:
.If ($$arg0 .TypeEqual ConsoleApp12.Foo && $$arg1 .TypeEqual System.Int32)
$$ arg0 e
$$ arg1 são os parâmetros com os quais o
Target é chamado:
Foo.o__0.p__0.Target((CallSite) Foo.o__0.p__0, <b>this</b>, <b>instance</b>);
Traduzido para humano, isso significa o seguinte:
Já verificamos que, se o primeiro parâmetro for do tipo
Foo e o segundo for
Int32 , você poderá chamar com segurança o
Boo ((objeto) $$ arg1) .
.Return #Label1 { .Block() { .Call $$arg0.Boo((System.Object)((System.Int32)$$arg1)); .Default(System.Object) }
Nota: no caso de um erro de ligação, o bloco Label1 se parece com isso: .Return #Label1 { .Throw .New Microsoft.CSharp.RuntimeBinderException("NoSuchMember")
Essas verificações são chamadas de
restrições .
Existem dois tipos de
restrições : por tipo de objeto e por instância específica do objeto (o objeto deve ser exatamente o mesmo). Se pelo menos uma das restrições falhar, teremos que verificar novamente a expressão dinâmica quanto à validade, para isso chamaremos o delegado de
Atualização . De acordo com o esquema já conhecido, ele executará a ligação com novos tipos e salvará o novo
ExpressionTree no
Target .
Cache
Já descobrimos que o
destino é um
cache L0 . Cada vez que o
Target é chamado, a primeira coisa a fazer é passar pelas restrições já armazenadas nele. Se as restrições falharem e uma nova ligação for gerada, a regra antiga será simultaneamente para
L1 e
L2 . No futuro, quando você perder o cache
L0 , as regras de
L1 e
L2 serão pesquisadas até encontrar uma adequada.
- L1 : As últimas dez regras que deixaram L0 (armazenadas diretamente no CallSite )
- L2 : As últimas 128 regras criadas usando uma instância específica do fichário (que é CallSiteBinder , exclusiva para cada CallSite )
Agora, finalmente, podemos adicionar esses detalhes em um único todo e descrever na forma de um algoritmo o que acontece quando
Foo.Bar (someDynamicObject) é chamado :
1. É criado um fichário que lembra o contexto e o método chamado no nível de suas assinaturas
2. Na primeira vez que a operação é chamada,
ExpressionTree é criado, que armazena:
2.1
Limitações . Nesse caso, haverá duas restrições no tipo de parâmetros de ligação atuais
2.2
Função objetivo : ou
lançar alguma exceção (neste caso, é impossível, pois qualquer
dinâmica levará com sucesso ao objeto) ou uma chamada ao método
Bar3. Compile e execute o ExpressionTree resultante
4. Quando você recorda a operação, duas opções são possíveis:
4.1
Limitações trabalhadas : basta chamar
Bar4.2 As
limitações não funcionaram : repita a etapa 2 para novos parâmetros de ligação
Portanto, com o exemplo do tipo
Gerenciado , ficou aproximadamente claro como a
dinâmica funciona por dentro. No caso descrito, nunca perderemos o cache, pois os tipos são sempre os mesmos *, portanto,
Update será chamado exatamente uma vez quando o
CallSite for inicializado. Então, para cada chamada, apenas restrições serão verificadas e a função objetivo será chamada imediatamente. Isso está de acordo com nossas observações da memória: sem cálculo - sem vazamentos.
* Por esse motivo, o compilador gera seus CallSites para cada um: a probabilidade de perder o cache L0 é extremamente reduzidaÉ hora de descobrir como esse esquema difere no caso de objetos
COM nativos . Vamos dar uma olhada no
ExpressionTree :
.Lambda CallSite.Target<System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]>( Actionsss.CallSite $$site, ConsoleApp12.Foo $$arg0, System.Object $$arg1) { .Block() { .If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 }) { .Return #Label1 { .Block() { .Call $$arg0.Boo((System.__ComObject)$$arg1); .Default(System.Object) } } } .Else { .Default(System.Void) }; .Block() { .Constant<Actionsss.Ast.Expression>(IIF((($arg0 TypeEqual Foo) AndAlso {var Param_0; ... }), returnUnamedLabel_1 ({ ... }) , default(Void))); .Label .LabelTarget CallSiteBinder.UpdateLabel: }; .Label .If ( .Call Actionsss.CallSiteOps.SetNotMatched($$site) ) { .Default(System.Void) } .Else { .Invoke (((Actionsss.CallSite`1[System.Action`3[Actionsss.CallSite,ConsoleApp12.Foo,System.Object]])$$site).Update)( $$site, $$arg0, $$arg1) } .LabelTarget #Label1: } }
Pode-se ver que a diferença está apenas na segunda restrição:
.If ($$arg0 .TypeEqual ConsoleApp12.Foo && .Block(System.Object $var1) { $var1 = .Constant<System.WeakReference>(System.WeakReference).Target; $var1 != null && (System.Object)$$arg1 == $var1 })
Se, no caso do código
gerenciado , tivéssemos duas restrições no tipo de objetos, aqui vemos que a segunda restrição verifica a equivalência de instâncias por meio de
WeakReference .
Nota: A restrição de instância, além dos objetos COM, também é usada para o TransparentProxyNa prática, com base no nosso conhecimento da operação do cache, isso significa que toda vez que recriarmos um objeto
COM em um loop, perderemos o cache
L0 (e
L1 / L2 também, porque as regras antigas com links serão armazenadas lá para instâncias antigas). A primeira suposição que você pergunta na cabeça é que o cache de regras está fluindo. Mas o código é bem simples e está tudo bem: as regras antigas são excluídas corretamente. Ao mesmo tempo, o uso de
WeakReference no
ExpressionTree não impede que o
GC colete objetos desnecessários.
O mecanismo para salvar regras no cache L1: const int MaxRules = 10; internal void AddRule(T newRule) { T[] rules = Rules; if (rules == null) { Rules = new[] { newRule }; return; } T[] temp; if (rules.Length < (MaxRules - 1)) { temp = new T[rules.Length + 1]; Array.Copy(rules, 0, temp, 1, rules.Length); } else { temp = new T[MaxRules]; Array.Copy(rules, 0, temp, 1, MaxRules - 1); } temp[0] = newRule; Rules = temp; }
Então, qual é o problema? Vamos tentar esclarecer a hipótese: um vazamento de memória ocorre em algum lugar ao vincular um objeto
COM .
Experiências, parte 2
Novamente, vamos passar de conclusões especulativas para experimentos. Primeiro, vamos repetir o que o compilador faz por nós:
Verificamos:

O vazamento foi preservado. Justo. Mas qual o motivo? Depois de estudar o código dos fichários (que deixamos entre colchetes), fica claro que a única coisa que afeta o tipo de nosso objeto é a opção de restrição. Talvez isso não seja uma questão de objetos
COM , mas um fichário? Não há muitas opções, vamos provocar várias ligações para o tipo
gerenciado :
while (true) { object instance = Activator.CreateInstance(typeof(int)); var autogeneratedBinder = Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "Boo", null, typeof(Foo), new CSharpArgumentInfo[2] { CSharpArgumentInfo.Create( CSharpArgumentInfoFlags.UseCompileTimeType, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }); var callSite = CallSite<Action<CallSite, Foo, object>>.Create(autogeneratedBinder); callSite.Target(callSite, this, instance); }

Uau! Parece que o pegamos. O problema não é de todo com o
objeto COM , como nos pareceu inicialmente, apenas por causa das limitações da instância, este é o único caso em que a ligação ocorre muitas vezes dentro do nosso loop. Em todos os outros casos, peguei o
cache L0 e vinculei uma vez.
Conclusões
Vazamento de memória
Se você trabalha com variáveis
dinâmicas que contêm
COM nativo ou
TransparentProxy , nunca as passe como parâmetros de método. Se você ainda precisar fazer isso, use a conversão explícita para
objetar e o compilador ficará para trás.
Errado :
dynamic com = Activator.CreateInstance(comType);
Corretamente :
dynamic com = Activator.CreateInstance(comType);
Como precaução adicional, tente instanciar esses objetos o mais raramente possível. Real para todas as versões do
.NET Framework . (Por enquanto) não é muito relevante para.
NET Core , pois não
há suporte para objetos
COM dinâmicos .
Desempenho
É do seu interesse que as falhas de cache ocorram o mais raramente possível, pois nesse caso não há necessidade de encontrar uma regra adequada nos caches de alto nível. As falhas no cache
L0 ocorrerão principalmente no caso de uma incompatibilidade do tipo de objeto
dinâmico com as restrições preservadas.
dynamic com = GetSomeObject(); public object GetSomeObject() {
No entanto, na prática, você provavelmente não notará a diferença no desempenho, a menos que o número de chamadas para essa função seja medido em milhões ou se a variabilidade de tipos não for extraordinariamente grande. Os custos em caso de
perda no cache
L0 são tais,
N é o número de tipos:
- N <10. Se você errar, repita apenas as regras de cache L1 existentes
- 10 < N <128 . Enumeração de cache L1 e L2 (máximo de 10 e N iterações). Criando e preenchendo uma matriz de 10 elementos
- N > 128. Repita o cache L1 e L2 . Crie e preencha matrizes de 10 e 128 elementos. Se você perder o cache L2 , volte a ligar
No segundo e terceiro casos, a carga no GC aumentará.
Conclusão
Infelizmente, não encontramos um motivo real para o vazamento de memória, isso exigirá um estudo separado do fichário. Felizmente, o
WinDbg fornece uma dica para uma investigação mais aprofundada: algo ruim acontece no
DLR . A primeira coluna é o número de objetos

Bônus
Por que a conversão de objetos evita explicitamente um vazamento?Qualquer tipo pode ser convertido em
objeto , portanto, a operação deixa de ser dinâmica.
Por que não há vazamentos ao trabalhar com campos e métodos de um objeto COM?É assim que o
ExpressionTree se parece com o acesso ao campo:
.If ( .Call System.Dynamic.ComObject.IsComObject($$arg0) ) { .Return #Label1 { .Dynamic GetMember ComMarks(.Call System.Dynamic2.ComObject.ObjectToComObject($$arg0)) } }