André Alves de Lima

Talking about Software Development and more…

Evitando memory leaks no .NET com Dispose e blocos using

O .NET normalmente faz um ótimo trabalho no que diz respeito ao gerenciamento de memória. Porém, um grave erro cometido pela maioria dos desenvolvedores que trabalham com essa plataforma é só se preocupar com a alocação de memória quando ela vira um problema (ou seja, quando a aplicação trava por falta de memória). Dois conceitos extremamente importantes relacionados à alocação e liberação de memória no .NET são o método Dispose e os blocos using. No vídeo de hoje eu explico para você a importância desses conceitos e como utilizá-los. Confira:

Alocação e liberação de memória no .NET

Apesar do Garbage Collector do .NET facilitar muito a nossa vida, nós temos que seguir alguns protocolos, senão ele não funcionará da maneira esperada. Basicamente, ao declararmos variáveis na nossa aplicação e atribuirmos um valor para elas, nós estamos alocando memória.

Quando falamos de variáveis simples (como strings, números, etc), nós não sentiremos praticamente nenhum impacto no consumo de memória, uma vez que a quantidade de memória utilizada por esses tipos de objeto é muito pequena. Porém, quando começamos a trabalhar com objetos mais complexos (por exemplo, ao carregarmos informações do banco de dados em uma DataTable), a alocação de memória pode começar a se transformar em um problema.

A alocação da memória é algo inevitável, afinal, temos que armazenar a informação em memória de um jeito ou de outro. Porém, mais importante do que a alocação é a liberação da memória quando não estamos mais utilizando as informações.

Normalmente nós não temos que nos preocupar com a liberação de memória no .NET, uma vez que o Garbage Collector faz um ótimo trabalho detectando as variáveis que não estão mais sendo utilizadas (ou seja, as que estão fora de escopo) e libera a memória automaticamente. Porém, em alguns casos o Garbage Collector não consegue detectar facilmente se uma variável realmente não está mais sendo utilizada. É aí que entra o método Dispose e os blocos using.

Exemplo clássico: carregamento de grid

Para testarmos a alocação e liberação de memória, criei uma aplicação Windows Forms muito simples, na qual teremos dois botões no formulário, além de um objeto do tipo DataGridView:

Um exemplo muito comum que encontraremos em praticamente qualquer aplicação que trabalhe com banco de dados é o carregamento de um grid com informações vindas do banco. Muito provavelmente você deve utilizar um código parecido com este para fazer esse tipo de carregamento:

// C#
lblCarregando.Visible = true;
Application.DoEvents();

var conn = new System.Data.SQLite.SQLiteConnection("Data Source=db.db;");
conn.Open();
var comm = new System.Data.SQLite.SQLiteCommand(conn);
comm.CommandText = "SELECT * FROM Tabela";
var dataTable = new DataTable();
var reader = comm.ExecuteReader();
dataTable.Load(reader);
dataGridView.DataSource = dataTable;

lblCarregando.Visible = false;
' VB.NET
LblCarregando.Visible = True
Application.DoEvents()

Dim Conn = New System.Data.SQLite.SQLiteConnection("Data Source=db.db;")
Conn.Open()
Dim Comm = New System.Data.SQLite.SQLiteCommand(Conn)
Comm.CommandText = "SELECT * FROM Tabela"
Dim DataTable = New DataTable()
Dim Reader = Comm.ExecuteReader()
DataTable.Load(Reader)
DataGridView.DataSource = dataTable

LblCarregando.Visible = False

O código acima funciona perfeitamente, carregando as informações do banco em uma DataTable e exibindo o seu conteúdo no DataGridView (configurando o DataSource). Porém, nós temos alguns problemas ao utilizarmos o código dessa maneira. O primeiro deles é que a conexão e o DataReader não foram fechados. Além disso, o que acontece se nós executarmos exatamente esse mesmo código múltiplas vezes (para atualizar as informações do grid, por exemplo)?

Ao executarmos esse método várias vezes, a memória da DataTable anterior não será liberada, o que causará um acúmulo na utilização de memória para a aplicação. Quando estamos carregando pouca informação no grid, nós nem sentiremos muito o impacto. Porém, se estivermos carregando uma quantidade considerável de informações (como as 1 milhão de linhas que temos na aplicação de exemplo), a memória do processo só subirá a cada chamada e, uma hora ou outra, a aplicação travará por falta de memória.

Interface IDisposable

No .NET nós temos uma interface chamada “IDisposable” que diz respeito à questão da liberação de recursos. Toda classe que aloca memória com recursos não gerenciados (ou que utiliza recursos que devem ser corretamente fechados) deve implementar essa interface, onde os recursos deverão ser liberados na implementação do método “Dispose“.

Quando desenvolvemos as nossas classes, dificilmente nós teremos que nos preocupar em implementar essa interface nos tipos da nossa aplicação. Porém, inúmeras classes do .NET implementam essa interface, e quando nós utilizamos essas classes, nós temos que chamar o método Dispose por dois motivos.

O primeiro motivo é que só assim a memória alocada para os recursos não gerenciados será liberada. Já o segundo motivo é que, ao chamarmos o método Dispose de um objeto que implementa essa interface, ele ficará mais visível para que o Garbage Collector libere a memória alocada por ele.

Para saber se uma classe implementa a interface IDisposable, navegue pela definição dela (clicando no nome da classe e apertando a tecla F12) e tente encontra-la em algum nível da hierarquia. Se você encontrar, isso significa que você deve chamar o método Dispose dos objetos desse tipo (ou deve declará-los com um bloco using). Esse é o caso da maioria das classes do ADO.NET (por exemplo, DbConnection, DbCommand, DbDataReader, etc).

Melhorando o código com blocos using

A primeira coisa que temos que ajustar no código apresentado anteriormente é chamarmos o método Dispose dos objetos do ADO.NET (Connection, Command, DataReader, etc) quando não precisamos mais deles (ou seja, no final do carregamento). Nós poderíamos simplesmente chamar o método Dispose manualmente na última linha do método, porém, nós podemos fazer isso de uma maneira mais elegante utilizando blocos using.

Quando declaramos uma variável utilizando um bloco using, nós estamos implicitamente declarando que ela só estará sendo utilizada dentro daquele bloco. O .NET chamará automaticamente o método Dispose desses objetos uma vez que o bloco using for finalizado. Veja como é que ficaria uma versão melhorada do acesso a dados do nosso exemplo, utilizando blocos using para declarar as variáveis relacionadas ao ADO.NET:

            // C#
            using (var conn = new System.Data.SQLite.SQLiteConnection("Data Source=db.db;"))
            {
                conn.Open();

                using (var comm = new System.Data.SQLite.SQLiteCommand(conn))
                {
                    comm.CommandText = "SELECT * FROM Tabela";
                    var dataTable = new DataTable();
                    using (var reader = comm.ExecuteReader())
                    {
                        dataTable.Load(reader);
                    }
                    dataGridView.DataSource = dataTable;
                }
            }
        ' VB.NET
        Using Conn = New System.Data.SQLite.SQLiteConnection("Data Source=db.db;")
            Conn.Open()

            Using Comm = New System.Data.SQLite.SQLiteCommand(Conn)
                Comm.CommandText = "SELECT * FROM Tabela"
                Dim DataTable = New DataTable()
                Using Reader = Comm.ExecuteReader()
                    DataTable.Load(Reader)
                End Using
                DataGridView.DataSource = dataTable
            End Using
        End Using

Esse código já é bem melhor do que o anterior, uma vez que nós estamos descartando todos os objetos do ADO.NET que foram utilizados no processo de aquisição dos dados. Por exemplo, nessa segunda versão a conexão e o DataReader serão fechados automaticamente uma vez que os blocos using respectivos forem finalizados.

Entretanto, somente essa alteração no código não bastará para que a memória das DataTables anteriores seja descartada mais rapidamente. Uma segunda melhoria no código seria fazermos um Clear e um Dispose na DataTable que estava servindo de DataSource do grid no ciclo anterior. Fazemos isso através do seguinte código (que você deve adicionar antes de carregar os dados novamente do banco):

            // C#
            var dataSourceAnterior = dataGridView.DataSource as IDisposable;
            var dataTableAnterior = dataGridView.DataSource as DataTable;
            dataGridView.DataSource = null;
            if (dataSourceAnterior != null)
            {
                dataSourceAnterior.Dispose();
            }
            if (dataTableAnterior != null)
            {
                dataTableAnterior.Clear();
            }
        ' VB.NET
        Dim DataSourceAnterior = TryCast(DataGridView.DataSource, IDisposable)
        Dim DataTableAnterior = TryCast(DataGridView.DataSource, DataTable)
        DataGridView.DataSource = Nothing
        If DataSourceAnterior IsNot Nothing Then
            DataSourceAnterior.Dispose()
        End If
        If dataTableAnterior IsNot Nothing Then
            dataTableAnterior.Clear()
        End If

Agora sim. Com essas alterações, mesmo se clicarmos no botão para carregarmos os dados diversas vezes, a memória utilizada pelos dados anteriores será descartada pelo Garbage Collector.

Segundo exemplo: exibição de formulários

Um segundo exemplo clássico é a exibição de formulários. Se você der uma olhada na classe “Form“, você verá que em algum nível da hierarquia ela herda de IDisposable. Ou seja, nós devemos chamar o método Dispose de um formulário ou devemos utilizá-la dentro de um bloco using (apesar que esse método é chamado automaticamente na maioria dos casos quando executamos o método Close).

Veja um exemplo de chamada de formulário sem a utilização de bloco using:

// C#
for (int c = 0; c < 5; c++)
{
	var form = new Form2();
	form.Show();
	form.Close();
}
' VB.NET
For C As Integer = 0 To 4
	Dim Form = New Form2()
	Form.Show()
	Form.Close()
Next

Esse código simplesmente abre e fecha 5 vezes um formulário qualquer (que no nosso projeto de exemplo carrega um grid com 500 mil linhas). Apesar de não fazer nenhum efeito direto (porque estamos chamando o método Close que automaticamente chamará o Dispose), o código fica mais “correto” se declararmos o formulário dentro de um bloco using:

            // C#
            for (int c = 0; c < 5; c++)
            {
                using (var form = new Form2())
                {
                    form.Show();
                    form.Close();
                }
            }
        ' VB.NET
        For C As Integer = 0 To 4
            Using Form = New Form2()
                Form.Show()
                Form.Close()
            End Using
        Next

Além disso, é importante descartarmos recursos não utilizados (por exemplo, a DataTable que serviu de fonte de dados para o grid) no método Dispose do formulário. No C# você encontra esse método dentro do arquivo de designer do formulário:

Já no VB.NET, você encontra esse método expandindo o nome do formulário:

Baixe o projeto de exemplo

Para baixar o projeto de exemplo desse artigo, assine a minha newsletter. Ao fazer isso, além de ter acesso ao projeto, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário no final do artigo.

Concluindo

Apesar do .NET fazer um ótimo trabalho quanto à alocação e liberação automática de memória, é importante que nós lembremos de chamar o método Dispose em objetos que implementam a interface IDisposable. Dessa forma, além de descartarmos recursos não gerenciados que tiverem sido utilizados pela instância, nós estaremos também implicitamente dizendo ao Garbage Collector que nós não estamos mais utilizando aquele objeto, o que fará com que ele seja descartado da memória mais rapidamente.

Você já conhecia essa interface? Sabia da existência dos blocos using para facilitar a utilização de objetos desse tipo? Fico aguardando os seus comentários logo abaixo!

Até a próxima!

André Lima

Photo by Pixabay used under Creative Commons
https://pixabay.com/en/startup-start-up-notebooks-creative-593327/

Photo by Pixabay used under Creative Commons
https://pixabay.com/en/printed-circuit-board-memory-green-1911693/

Song Rocket Power Kevin MacLeod (incompetech.com)
Licensed under Creative Commons: By Attribution 3.0 License
http://creativecommons.org/licenses/by/3.0/

Newsletter do André Lima

* indicates required



Powered by MailChimp

16 thoughts on “Evitando memory leaks no .NET com Dispose e blocos using

  • Pablo Sousa disse:

    Bom Dia André Lima!!!

    André parabéns muito bom essa rotina, e quase isso que estava para lhe perguntar parece que leu meus pensamentos.
    André minhas aplicações faço em VB.NET visual studio 2010, só que na parte de código uso bastante as funcionalidades do VS2010, por exemplo em um formulario tenho ao lado os bancos de dados conectados eu faço arrastando para o formulario ai já cria todos os codigos beleza, mas estou tendo alguns problemas no fechamento do formulario e na nova abertura, exemplo:
    Vou tentar explicar o que preciso.
    No load do form quando abre a 1º vez o formulario ele traz todos os componentes certinho, mas se o formulario for de cadastro de Pedido, e nele tem uma chamada para mas 2 formularios um de clientes, outro de produtos, ai esse 1º form abre começa a fazer o pedido, abre o 2º form clientes faço uma pesquisa o form2 clientes para pegar os dados e colocar no 1º, ai fecha o form clientes, abre o form produtos faço uma pesquisa pego o ´produtos e jogo os dados no 1º form pedido e fecho o form 2, ai quando eu salvo os dados do pedido 1 por exemplo, deu tudo certo os dados foi salvo colocou os codigos novamente no pedido, mas ai que acontece o erro e não consegui resolver ele, tive que fazer uma ganbiarra.
    O erro acontece quando eu peço para abrir o form 2 clientes faço a pesquisa e quando peço para colocar os dados no 1º formulario as textbox fica tudo em branco, não aparece erro só não preenche os textbox.
    isso se eu for fazer um pedido na sequencia do outro sem fechar o 1º formulario pedido.
    André quando comecei a fazer sistemas comecei em VBA excel, lá era simples era só fazer assim quando terminava um pedido, unload.me , ai pedido.show , ele fechava o form e abria novamente.
    Em VB.NEt tentei assim,
    Dim novform as new = pedido
    novform.show
    me.dispose
    Assim não da certo, ai fiz assim mas acho que não um jeito certo na rotina,
    Fiz um formulario Atualizar, ai quando eu salvo o pedido eu chamo esse formulario, fecho o de pedido, fecho o de mensagens e ai abre o de pedido, esquesito demais, mas foi assim que consegui, será que tem outra forma mais facil de fazer isso.
    Andre o codigo para pegar um dado de uma textbox de um formulario para outro e simples.
    Acho que isso acontece no fechamento dos formularios, mas não consegui achar erro algum.
    Agora se eu fazer um pedido e fechar o formulario e abrir de novo tudo funciona, mais ai o cliente fica louco, imagina.
    André não tem como quando clicar para salvar pegar o codigo que esta no designer do form e executar ele para tudo iniciar do zero???
    André já procurei muito sobre isso no google, no forum technet já fiz essa pergunta varias vezes mas nunca consegui uma resposta clara sobre isso, as vezes você tem uma solução para isso, acho que muitos programadores já sofreu com isso, ou esta sofrendo, ou vai sofrer.
    Desde já agradeço atenção, muito obrigado!!!

    • andrealveslima disse:

      Olá Pablo, obrigado pelo comentário!

      Não consegui entender muito bem o seu problema.. Será que você conseguiria preparar um projetinho de exemplo para eu dar uma olhada e entender melhor?

      Abraço!
      André Lima

  • Tony Rodrigues disse:

    Tá de parabéns esse post!!!
    Esse vou ficar estudando até compreendê-lo 100%.

    Cara, onde eu acho o link do exemplo? Já sou assinante da Newsletter.

    • andrealveslima disse:

      Maravilha, Tony! Qualquer dúvida é só entrar em contato..

      Quanto ao projeto de exemplo, acabei de mandar o link no seu e-mail..

      Abraço!
      André Lima

  • Andrá Lucio disse:

    Parabéns ótimo trabalho, seus artigos tem me ajudado muito, pois estou aprendendo tudo sozinho..como não sou da área sua didática é ótima. Mais uma vez parabéns pelo ótimo trabalho.

  • deuzivaldo disse:

    Bom dia professor muito bom parabéns e muito obrigado

  • Victor Hugo disse:

    Uma dúvida,
    Se eu apenas declarar um bloco using em um objeto, os que também estão dentro, deverá ter um bloco using ou serão disposed ?

    • andrealveslima disse:

      Olá Victor!

      Como respondi lá no seu comentário no Youtube, o dispose só será chamado automaticamente para o objeto que foi instanciado no cabeçalho do bloco.. Ou seja, você terá que fazer vários blocos using, um para cada objeto que deverá ser descartado..

      Abraço!
      André Lima

  • Valdemar Paulo disse:

    Olá André!

    De facto o bloco using é muito útil, eu já tinha lido a respeito do mesmo e na altura não havia entendido bem, mas neste artigo deste uma explicação muito pratica e objectiva que ajudo-me a perceber muito mais do que eu imaginava muito obrigado, resolveu o problema deixou de dar a Sms o ExecuteRidear Requer uma ligação aberta ou fechada! valeu muito eu estava a ficar desesperado com a situação, estou muito grato por disponibilizares artigos como estes.
    E que Deus te de forças para continuares a expandir os teus conhecimento, olha que tens feito muito por nós…

    • andrealveslima disse:

      Olá Valdemar!

      Muito obrigado pelo comentário! Fico extremamente feliz por ter conseguido te ajudar a resolver o seu problema com o DataReader.. Qualquer outra dúvida é só entrar em contato novamente..

      Abraço!
      André Lima

  • André trabalho mais com desenvolvimento Web e Xamarin, em meus projetos Web uso muito dispose, principalmente no uso do entity framework, mas as aplicações web que vejo no servidor estão sempre consumindo exageradamente memoria, tipo 1GB de ram. O que você recomenda para reduzir o consumo de memoria?

    • andrealveslima disse:

      Olá!

      Análise de consumo de memória é sempre muito complicado.. Cada projeto é um projeto, não tem muito o que recomendar sem conhecer a aplicação.. Normalmente memory leaks acontecem por falta de dispose mesmo..

      A primeira coisa que você precisa fazer é identificar as ações que estão causando esse alto consumo de memória.. Você não consegue colocar essa mesma aplicação em um outro servidor de testes e ir fazendo operações nela para tentar descobrir a fonte do problema?

      Além dessa ideia, a única outra dica que eu posso dar é tentar utilizar alguma ferramenta especializada nesse tipo de análise, como o dotMemory da JetBrains:

      dotMemory

      Abraço!
      André Lima

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *