Introdução
Uma das partes mais importantes no desenvolvimento em .Net é o conhecimento da forma como a CLR ("Common Language Runtime") aloca e liberta memória, e sobretudo como os objectos são destruídos no final do seu ciclo de vida.
COM e "Reference Counter"
Antes de estudar a forma como o .Net lida com a destruição de objectos, é importante entender a forma como os objectos COM se comportam a este respeito.
Os objectos COM mantêm uma localização de memória conhecida como "reference Counter", que recebe o valor 1 quando o objecto é criado e uma referência é assignada a uma variável. Sempre que outra variável é assignada ao mesmo objecto, o "reference counter" é incrementado em uma unidade. Sempre que uma desta variáveis é assignada a "Nothing" o "reference counter" é reduzido em uma unidade.
Desta forma, o "refence counter" contém a cada instante o número de variáveis que apontam para um dado objecto. Sempre que uma variável deixa de apontar para um objecto, é verificado se o "reference counter" passa de 1 para 0. Se esta condição se verificar, o objecto é destruído. De certa forma pode afirmar-se que os objectos COM são responsáveis pela sua própria vida.
Esta gestão de vida de objectos, é pesada e propensa a erros (nas linguagens em que a gestão do "reference counter" é feita pelo programador), tendo um impacto negativo na "performance" das aplicações.
Para além das questões de performance, o principal problema da gestão de ciclo de vida dos componentes COM é conhecido como "referências circulares".
Uma "referência circular" ocorre quando dois componentes se mantêm vivos por possuírem variáveis que se referenciam um ao outro, não estando a ser utilizadas por nenhuma outra variável da aplicação. Quando isto ocorre, a memória apenas é libertada quando a aplicação termina. Este é o principal responsável por problemas de "memory leakage" em componentes COM.
O .Net foi desenhado com o objectivo de anular definitivamente a necessidade de manter um "reference counter" e todos os problemas a este associados. Os objectos criados em .Net não possuem "reference counter", mas são mantidos numa área especial de memória designada "managed heap". Esta implementação implica que os objectos deixam de ser responsáveis pela gestão do seu ciclo de vida, sendo outro elemento, o "GC – garbage collection process" o responsável por esta gestão.
"Garbage Collection Process"
A gestão de memória na "framework" .net depende de um processo complexo, denominado "Garbage Collection". Quando uma aplicação tenta alocar memória para um novo objecto, e o "heap" não tem espaço livre suficiente, a "framework" inicia o processo de "garbage collection".
O processo de "garbage collection" é realizado internamente por um objecto denominado "garbage collector". Este objecto percorre todos os objectos existentes no "heap" e marca os que estão actualmente referenciados por pelo menos uma variável da aplicação (este processo reconhece objectos referenciados indirectamente a partir de outros objectos). Após marcar todos os objectos referenciados, o "garbage collector" destrói todos os outros objectos, uma vez que não são acedidos nem directamente nem indirectamente de qualquer variável da aplicação.
Após libertar os objectos não utilizados, o "garbage collector" compacta o "heap" e disponibiliza a memória disponível para novos objectos.
Na grande maioria dos casos, a forma de lidar com o ciclo de vida dos objectos no .Net é significativamente mais eficiente que a utilizada nos componentes COM. Contudo introduz um novo problema: "Nondeterministic Finalization".
Um objecto COM controla o momento em que o seu "reference counter" passa de 1 para 0, ou seja, controla o momento em que deixa de ser utilizado pela aplicação. Quando esse momento surge, é disparado o evento "Class_terminate"(ou executado o "destructor" no caso do c++) e o código aqui existente pode proceder as acções de limpeza de objectos e libertação de recursos (como por exemplo ficheiros).
No caso do .Net, não existe controlo sobre o momento em que o objecto vai ser libertado, apenas se sabe que será algum tempo após a última variável que aponta para ele ser igualada a "Nothing". Assim não é possível utilizar no .Net um equivalente ao evento semelhante ao "Class terminate".
Torna-se assim necessário distinguir entre destruição lógica de um objecto (quando a aplicação liberta a última variável que o utiliza) e a sua destruição física (quando o objecto é realmente removido da memória).
"Nondeterministic Finalization"
Se a memória fosse o único recurso utilizado pelos objectos, a falta de controlo sobre o momento da destruição não seria propriamente um problema, pois a gestão de memória poderia garantir a sua reorganização sempre que uma aplicação necessitasse de um bloco maior.
A razão do problema deve-se a que os objectos normalmente utilizam outro tipo de recursos, tal como ficheiros, conexões a bases de dados, ligações a portas série ou paralelas, objectos internos, etc. Quando este tipo de recursos é utilizado, a sua libertação tem de ser feita com a maior urgência possível, de forma a garantir que outra parte da aplicação, ou outras aplicações os possam utilizar.
A solução para o problema passa por encontrar uma forma de executar código de limpeza de recursos quando os objectos são destruídos. Esta solução foi implementada através de dois métodos especiais: "Finalize" e "Dispose".
"Finalize"
O método "Finalize" é um método especial que é chamado pelo "garbage collector" exactamente antes de libertar a memória utilizada pelo objecto, ou seja, quando o objecto é fisicamente destruído. É o mecanismo mais semelhante ao evento "Class_terminate" (ou aos "destructors"), mas pode ser desencadeado vários minutos, ou mesmo horas após a libertação lógica dos recursos.
O método "Finalize" faz parte da classe "system.object" pelo que é herdado por todos os objectos da plataforma (Todos os objectos da plataforma herdam de "system.object"). Assim, para que seja possível incluir um "Finalize" personalizado para um objecto especifico, é necessário incluir as expressões "Overrides" e "Protected".
Protected Overrides Sub Finalize()
Debug.Writeline("O objecto vai ser destruido")
End Sub
O "Finalize" nunca deve ser utilizado para aceder a nenhum outro objecto, pois esse objecto pode já ter sido destruído (A ordem pela qual os objectos são reclamados e destruídos pelo "garbage collector" é impossível de prever). O único objecto que pode ser utilizado com segurança a partir do método "finalize" é o objecto base, através do "MyBase".
Como regra geral, é seguro invocar métodos estáticos a partir do método "finalize", excepto quando a aplicação esta a ser terminada, pois neste caso a plataforma .Net pode já ter destruído o tipo que define o objecto estático (Por exemplo, não deve ser utilizado o console.writeline pois o objecto "Console" pode já ter sido destruído. O objecto "Debug" é sempre um dos últimos objectos a ser destruído, sendo por isso preferível).
No entanto é possível determinar se a aplicação esta a ser terminada:
Protected Overrides Sub Finalize()
If Not Environment.HasShutDownStarted Then
'É seguro aceder a métodos estáticos
End If
End Sub
Os objectos que expõem um método "Finalize" normalmente necessitam de dois ciclos do "garbage collection" para serem reclamados e a sua memória libertada. Isto acontece pois o código dentro do método "finalize" pode assignar o objecto corrente (através do "Me") a uma variável global. Se o objecto fosse reclamado nesta fase a variável global ficaria com uma referência inválida.
A criação de objectos com um método "finalize" requer mais ciclos de CPU. É importante compreender que a finalização de objectos tem um custo, e como tal deve ser utilizada apenas quando for realmente necessária.
"Dispose"
O método "Dispose" permite colmatar a falta de verdadeiros destrutores no .Net.
Por convenção, uma classe bem desenhada deve expor um método chamado "Dispose", que permite a clientes "bem comportados" libertar manualmente todos os recursos utilizados (tal como ficheiros, ligações a bases de dados, etc.). Este método deve ser chamado pelo cliente, imediatamente antes de libertar a variável que aponta para o objecto, em vez de ficar à espera do próximo ciclo do "garbage collection".
As classes que disponibilizarem este método, tem de implementar o interface "IDisposable" que expõe apenas o método "Dispose".
Public Class Person
Implements IDisposable
Public Sub Dispose() Implements IDisposable.Dispose
'Código de limpeza
End Sub
End Class
Exemplo de utilização da classe anterior por um cliente "bem comportado":
'Cria o objecto
Dim obj As New Person
'Utiliza o objecto …
'…..
'Liberta os recursos
Obj.dispose
'Destrói a variável
Obj=Nothing
O Visual Basic permite a utilização do "Using", que garante que o método "Dispose" é chamado automaticamente, mesmo que o código gere uma excepção. A única limitação é que a excepção não é apanhada para tratamento especifico:
'Cria o objecto
Using obj as new person()
'Utiliza o objecto …
'…..
End Using
Por norma, o método "dispose" de um objecto deve invocar o método "dispose" de todos os objecto utilizados internamente, e que estão "escondidos" do cliente.
Por exemplo, se a classe "Person" utilizasse internamente um objecto "System.timers.timer", o método "dispose" da classe "Person" deveria chamar o método "dispose" do objecto "Timer":
Public Class Person
Implements IDisposable
Public Sub Dispose() Implements IDisposable.Dispose
'Código de limpeza
MyTimer.Dispose()
End Sub
End Class
A implementação da regra definida no ponto anterior, e o facto de alguns objectos serem partilhados por múltiplos clientes pode levar a que o método "dispose" seja chamado mais do que uma vez.
Por definição, o método "Dispose" não deve gerar qualquer erro se for chamado mais do que uma vez, embora deva ignorar todos os pedidos feitos após o primeiro ser recebido (Isto pode ser conseguido utilizando uma variável da classe):
Public Class Person
Implements IDisposable
Private Disposed As Boolean = False
Public Sub Dispose() Implements IDisposable.Dispose
If Disposed Then Exit Sub
Disposed = True 'Garante que só executa uma vez
'Código de limpeza
End Sub
End Class
Por definição, se for chamado um método que não o "dispose" num objecto para o qual já foi chamado o "Dispose" pelo menos uma vez, deve ser gerada uma excepção do tipo "ObjectDisposeException". Este comportamento pode ser obtido utilizando a mesma variável da classe:
Public Class Person
Implements IDisposable
Private Disposed As Boolean = False
Public Sub UmMetodo()
If Disposed Then Throw New ObjectDisposedException("Person")
'…
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
If Disposed Then Exit Sub
Disposed = True 'Garante que só executa uma vez
'Código de limpeza
End Sub
End Class
Combinar "Dispose" e "Finalize"
Tipicamente, uma recurso é alocado de uma de duas formas (á excepção da memória):
- Invocando código "unmanaged" (por exemplo ao utilizar uma função da API do windows)
- Através da criação de uma instancia de um objecto da framework .Net que expõe o recurso
A técnica utilizada condiciona a decisão sobre qual o melhor local para libertar recursos "Dispose" ou "Finalize":
- Se for alocado um recurso que não memória, é necessário implementar o método "Dispose" (independentemente de se o recurso foi alocado através de código "managed" ou "unmaged".
- O método "Finalize" apenas tem de ser implementado se o objecto alocar um recurso "unmanaged" directamente.
Nota: A implementação dos métodos "Dispose" ou "Finalize" apenas é realmente obrigatória se a classe possuir uma referência para o recurso através de uma variável da classe. Se cada método alocar e desalocar os seus próprios recursos (por exemplo com "Using", não é realmente necessário implementar qualquer uma destas técnicas.
A decisão sobre a técnica de finalização mais adequada pode variar pelos seguintes casos:
- Nem o "Dispose" nem o "Finalize": Se a classe utiliza apenas memória ou outros recursos que não necessitam desalocação explicita, ou se cada método alocar e desalocar os seus recursos internamente.
- Apenas o "Dispose": Se a classe aloca recursos para além da memória, através de outros objectos .Net, e se pretende providenciar aos clientes uma forma de libertar estes recursos o mais rápidamente possivel.
- Ambos "Dispose" e "Finalize": Se a classe aloca um recurso directamente (tipicamente atarvés de uma DLL "unmaged") que requer desalocação explicita. A desalocação explicita é realizada no método "Finalize" mas o método "Dispose" permite ao cliente libertar os recursos de imediato.
- Apenas o "Finalize": Se a classe não tem nenhum recurso para libertar, mas é necessário realizar algum passo quando o objecto é finalizado (não é um cenário comum).
Exemplo do caso descrito no ponto 3 (Ambos "Dispose" e "Finalize"):
Public Class ClipboardWrapper
Implements IDisposable : Dim isOpen As Boolean
Private Declare Function OpenClipboard Lib "user32" Alias "OpenClipboard" (ByVal hwnd As Integer) As Integer
Private Declare Function CloseClipboard Lib "user32" Alias "CloseClipboard" () As Integer
Public Sub Open(ByVal hWnd As Integer)
If OpenClipboard(hWnd) = 0 Then Throw New Exception("Unable to open clipboard")
isOpen = True
End Sub
Public Sub Close()
If isOpen Then CloseClipboard()
isOpen = False
End Sub
Private Sub Dispose() Implements IDisposable.Dispose
Close()
End Sub
Protected Overrides Sub Finalize()
Close()
End Sub
End Class
"SuppressFinalize"
A técnica apresentada no exemplo anterior ainda não é perfeita. Neste caso o "garbage collector" irá chamar o método "finalize" quer o cliente tenha chamado o método "dispose" (ou o "close", também válido neste caso) ou não.
Como já foi descrito, o método "finalize" afecta negativamente a performance, como tal, em casos idênticos ao anterior (em que se utiliza o "Dispose" e o "Finalize"), deve ser utilizado um método da classe "GC", o "SupressFinalize", a partir do método "Dispose". Desta forma, se o cliente for "bem comportado" e chamar o método "Dispose", o "garbage collector" já não irá voltar a executa-lo no método "Finalize".
Public Class Person
Implements IDisposable
Private Disposed As Boolean = False
Public Sub Dispose() Implements IDisposable.Dispose
If Disposed Then Exit Sub
Disposed = True 'Garante que só executa uma vez
'Código de limpeza
GC.SupressFinalize(Me)
End Sub
End Class
Melhorar o mecanismo "Dispose" - "Finalize"
Outro problema deixado em aberto no exemplo anterior, e que pode ocorrer em classes mais complexas, é que o código de limpeza pode aceder a outros objectos referenciados pelo objecto corrente, e este código nunca deve ser executado a partir do método "Finalize" pois como descrito anteriormente, os objectos podem já não existir.
Esta situação pode ser resolvida movendo o código de limpeza para uma versão "overloaded" do método "dispose", que aceite um argumento booleano que identifica se a limpeza esta a ser feita a partir do "Dispose" ou do "finalize".
Public Sub Close()
Dispose(True)
End Sub
Private Sub Dispose() Implements IDisposable.Dispose
Dispose(True)
'Identifica como já tendo sido "Disposed"
Disposed = True
'Evita que o "Finalize" seja chamado
GC.SupressFinalize(Me)
End Sub
Protected Overrides Sub Finalize()
Dispose(False)
End Sub
Protected Overridable Sub Dispose(ByVal Disposing As Boolean)
If Disposed Then Exit Sub
If Disposing = True Then
'Aqui é seguro aceder a outros objectos !!
' …
End If
'Aqui pode ser colocado o código que pode ser executado nos dois casos
End Sub
O .Net permite implementar o "IDisposable Interface" de uma forma bastante rápida, gerando todo o template automáticamente, bastando escrever "Impements IDisposable".
De notar que nenhum "Finalize" é criado automaticamente. Este é o comportamento correcto, pois tal como já foi referido, apenas as classes que utilizam recursos "unmanaged" necessitam deste método, e uma vez que o seu uso tem impacto negativo na performance, a sua utilização não é o "default".
"Finalizers" em classes derivadas
Se uma classe herdar de outra classe, em que os métodos "Dispose" e "Finalize" estão devidamente implementados, e esta classe derivada não alocar nenhum novo recurso "unmanaged", não é necessário qualquer esforço adicional, uma vez que os métodos definidos na classe base são herdados pela nova classe.
Contudo, se a nova classe alocar novos recursos "unmanaged" é necessário redefinir estes métodos, de forma a libertar os recursos usados pela classe derivada.
Seguindo o modelo do exemplo anterior, em que apenas um método era utilizado, tanto para o "dispose" como para o "finalize", apenas é necessária a redefinição de um método.
Protected Overrides Sub Dispose(ByVal disposing As Boolean)
If Disposed Then Exit Sub
Try
If disposing Then
' The object is being disposed, not finalized. It is safe to access other
' objects (other than the base object) only from inside this block.
'…
End If
' Perform clean up chores that have to be executed in either case.
'…
Finally
' Call the base class's Dispose method in all cases.
MyBase.Dispose(disposing)
End Try
End Sub
Caso prático
Pretende-se desenvolver uma pequena aplicação que demonstre o ciclo de vida dos objectos .Net
A lógica da aplicação é a seguinte:
- É desenvolvida uma classe chamada "Creatures", que implementa os métodos "dispose" e "finalize" e que contabiliza o número de instâncias: criadas, vivas, "Zombies" (mortas pela aplicação mas ainda não mortas pelo "Garbage collector") e mortas
- Um "form" que apresenta a informação ao utilizador.
Regras para desenvolvimento
- Criar um novo projecto com o template "Windows Application"
-
Criar uma classe chamada "Creature" que implementa o interface "IDisposable" com as seguintes características:
- Um conjunto de variáveis estáticas, chamadas:Criados, Vivos, Zombies e mortos do tipo inteiro.
- Um construtor, onde são incrementadas as variáveis "Criados" e "Vivos"
- Um método "dispose" onde é retirada uma unidade a variável "Vivos" e adicionada uma a variável "Zombies"
- Um método "finalize" onde é retirada uma unidade à variável "Zombies" e adicionada à variável "Mortos"
-
Criar um "Form" com as seguintes características:
Sugestão de apresentação do Form
Solução
