Fontes:
Site: Microsoft,
Livro: Visual C# (Passo a Passo)

Comentários e melhoramentos: Cristiano de Castilhos.

 
1) O programador não tem controle sobre quando o destruidor é chamado, porque isso é determinado pelo coletor de lixo.

2) O coletor de lixo verifica se há objetos que não estão sendo usados pelo aplicativo.

3) Se ele considerar qualificados a destruição de um objeto, ele chama o destruidor (se houver) e libera a memória que foi utilizada para armazenar o objeto.

4) Destruidores também são chamados quando o programa é encerrado.

5) Em geral, o C# não requer tanto gerenciamento de memória (linguagem de alto nível) como é necessário quando você desenvolve com uma linguagem que não possui um coletor de lixo em tempo de execução (runtime). 

C#
    public class Contador 
    {         
        //Construtor 
        public Contador() 
        { 
            _conta++; 
        } 
 
        //Efetua a destruição dos objetos na memória (Pilha). 
        ~Contador() 
        { 
            _conta--; 
        } 
 
        public int Conta() 
        { 
            return _conta; 
        } 
 
        private static int _conta = 0; 
    } 
 
 

 O compilador converte o Destrutor para o código abaixo e coloca o corpo dele dentro do bloco try, seguido do bloco finally que chama o método Finalize da classe base.

C#
//Destrutor 
~Contador() 
{ 
    _conta--; 
} 
//O compilador converte este código em: 
protected override void Finalize() 
{ 
     try  
     {  
           //.... 
     } 
     finally 
     { 
           base.Finalize(); 
     } 
} 
 
  

 Lembrando: Somente podem ser destruídos os objetos que estão na Pilha !!!

C#
    public partial class Default : System.Web.UI.Page 
    { 
        protected void Page_Load(object sender, EventArgs e) 
        { 
 
        } 
 
        protected void Button1_Click(object sender, EventArgs e) 
        { 
            Contador conta = new Contador(); 
            Contador conta1 = new Contador(); 
            Contador conta2 = new Contador(); 
            Contador conta3 = new Contador(); 
 
            Label1.Text = conta.Conta().ToString(); 
        } 
    } 
 
 

A chamada do método conta.Conta() traz ao Label a quantidade de instâncias que possuo na memória. Quando eu clico no botão novamente por várias vezes percebo que em um determinado momento o contador começa do 4 novamente. Isto porque o Coletor de Lixo gerencia isso para você, ele mesmo determina a hora que será chamado o Destrutor.

Agora se você for comentar o Destrutor, você verá a diferença, você verá que o contador se tornará acumulativo.

Nós programadores não podemos explicitamente programar o momento certo que será destruído os objetos em C#, o coletor de lixo cuida disso, isto porque existem boas razões para os projetistas do C# terem decidido assim.
Se fosse sua responsabilidade gerenciar a memória poderia cometer vários erros:

1) Você poderia esquecer de destruir um objeto. Assim não desalocando memória, podendo se esgotar o espaço em memória !!!

2) Você poderia destruir um objeto ativo ou ainda sendo utilizado, isto poderia ocasionar referências ocilantes, ou perda nas referências entre a pilha e a heap.

3) Você poderia tentar destruir um objeto mais de uma vez e isto poderia ser desastroso.

Isto bota a segurança a aplicação em risco !!!

O Coletor de Lixo é responsável por:

1) Todo o objeto é destruído e seu destrutor executado. Quando o programa terminar todos os objetos são destruídos.

2) Cada objeto será destruído uma vez.

3) Cada objeto será destruído somente quando ele se torna inacesível - isto é, quando nenhuma referência referencia o objeto.

ISSO INSENTA O PROGRAMADOR DE SE PREOCUPAR COM TAREFAS DE BAIXO NÍVEL, PODENDO ASSIM SE PREOCUPAR MAIS COM A REGRA DE NEGÓCIO.

Funcionamento do Coletor de Lixo

 O coletor de lixo pode a qualquer momento atualizar as referências de objeto na memória. Só que ela não pode fazer se o objeto está sendo utilizado. Devido a isto ela segue alguns critérios:

1) Normalmente acontece no final da execução de um método.

2) Constrói um mapa de todos os objetos acessíveis. (
SOMENTE OS ACESSÍVEIS).

3) Verifica se algum dos objetos inacessíveis tem um destrutor que precisa ser executado (um processo chamado finalização). Todo
processo que tem uma finalização é colocado em uma fila especial chamada fila freachable (prununcia: efe-rítchbôl).

4) Desaloca objetos inacessíveis restantes (aqueles que não tem finalização), movendo os objetos acessíveis para a parte inferior da Heap, assim desfragmentando assim o Heap e liberando a memória na parte superior.

5) No final ele permite que outras threads reiniciem.

6) Finaliza os objetos inacessíveis que requerem finalização ou que estão na thread da fila freachable. 

Em resumo: Nós estamos trabalhando com uma linguagem de alto nível e não precisamos nos preocupar com estes detalhes, nosso foco é a regra de negócio. Tentem evitar o uso de destrutores, isso consome mais processamento, deixando a aplicação mais lenta, deixem que o Coletor de Lixo faça isso pra vocês.

Gerenciamento de Recursos

Nem sempre é aconselhável liberar um recurso em um destrutor, pois alguns recursos podem ser valiosos. Você somente irá liberar o recurso quando terá a certeza que não irá mais utilizá-lo.

Existem classes que  possuem seu próprio método de Descarte.

C#
    public partial class Default : System.Web.UI.Page 
    { 
        protected void Page_Load(object sender, EventArgs e) 
        { 
 
        } 
 
        protected void btAbrir_Click(object sender, EventArgs e) 
        { 
            StreamReader stream = new StreamReader(upFileUpload.FileContent);            
            string linha = null; 
            while ((linha = stream.ReadLine()) != null) 
            { 
                 Response.Write(linha + "<br>"); 
            }                 
            stream.Close(); 
        } 
    } 
 
 

Descarte seguro quando possui exceções

O código acima não será adequado porque se ocorrer algum erro durante a leitura do arquivo, será abortado a aplicação e o arquivo não será fechado. Se outra pessoa tentar abrí-lo, não conseguirá, pois o arquivo estará aberto, podendo ocorrer um erro para o mesmo.

Para garantir que o método descarte ou feche o arquivo poderá ser utilizado o bloco finally do try, como exemplo abaixo:
 

C#
    public partial class Default : System.Web.UI.Page 
    { 
        protected void Page_Load(object sender, EventArgs e) 
        { 
 
        } 
 
        protected void btAbrir_Click(object sender, EventArgs e) 
        { 
            StreamReader stream = new StreamReader(upFileUpload.FileContent); 
            try 
            {                 
                string linha = null; 
                while ((linha = stream.ReadLine()) != null) 
                { 
                    Response.Write(linha + "<br>"); 
                }                 
            } 
            finally 
            { 
                stream.Close(); 
            } 
        } 
    } 
 
 
 
  Encontramos outro problema !!! Poderei fechar o arquivo caso ele esteje fechado. Teríamos que fazer um
if (stream != null)
{
      stream.Close();
}

Olha todo o trabalho que passaríamos... imagina colocarmos isso em todos métodos que manipulam arquivos.
 
Para resolver esta situação utilizamos a instrução using, pois ela fornece um mecanismo limpo para controlar os tempos de vida dos recursos. Você pode criar objetos e os mesmos serem destruídos quando o bloco using acabar. Conforme exemplo abaixo:
 
 
 
C#
    public partial class Default : System.Web.UI.Page 
    { 
        protected void Page_Load(object sender, EventArgs e) 
        { 
 
        } 
 
        protected void btAbrir_Click(object sender, EventArgs e) 
        { 
            using (StreamReader stream = new StreamReader(upFileUpload.FileContent)) 
            {                
                string linha = null; 
                while ((linha = stream.ReadLine()) != null) 
                { 
                    Response.Write(linha + "<br>"); 
                }                 
            } 
        } 
    } 
 
 

A instrução using é precisamente equivalente à seguinte transformação abaixo:

C#
    public partial class Default : System.Web.UI.Page 
    { 
        protected void Page_Load(object sender, EventArgs e) 
        { 
 
        } 
 
        protected void btAbrir_Click(object sender, EventArgs e) 
        { 
            StreamReader stream = new StreamReader(upFileUpload.FileContent); 
            try 
            {                 
                string linha = null; 
                while ((linha = stream.ReadLine()) != null) 
                { 
                    Response.Write(linha + "<br>"); 
                }                 
            } 
            finally 
            { 
                if (stream != null) 
                { 
                    ((IDisposable)stream).Dispose(); 
                } 
            } 
        } 
    } 
 
 

Vantagens do Using
 
1) É facilmente escalonável se precisar descartar múltiplos recursos.
2) Não altera a lógica do código do programa.
3) Elimina o problema e evita a repetição.
4) É robusta. Se você utilizar a variável fora do escopo do using acontecerá um erro de compilação.

Chamando o método Dispose a partir de um destrutor
  
C#
    public partial class Default : System.Web.UI.Page, IDisposable 
    { 
        private bool _descartou = false; 
 
        ~Default() 
        { 
            Dispose(); 
        } 
 
        protected void Page_Load(object sender, EventArgs e) 
        { 
 
        } 
 
        public virtual void Dispose() 
        { 
            if (!this._descartou) 
            { 
                try 
                { 
                    // libera recursos já escassos aqui 
                } 
                finally 
                { 
                    this._descartou = true; 
                    GC.SuppressFinalize(this); 
                } 
            } 
        } 
 
        public void AlgumMetodo() 
        { 
            verificaSeDescartou(); 
        } 
 
        public void verificaSeDescartou() 
        { 
            if (this._descartou) 
            { 
                throw new ObjectDisposedException("Seu objeto foi descartado!!!"); 
            } 
        } 
    } 
 
 
 
1) A classe implementa a interface IDispose.
2) O destrutor chama Dispose.
3) O método Dispose é público e pode ser chamado a qualquer momento.
4) O método Dispose pode ser chamado de modo seguro muitas vezes. A variável disposed indica se o método já foi executado antes. O recurso escasso é liberado somente na primeira vez que o método executa.
5) O método Dispose chama o método estático GC.SuppressFinalize. Esse método faz com que o coletor de lixo pare de chamar o destrutor no seu objeto, porque o objeto agora foi finalizado.
6) Todos os métodos comuns da classe (como o AlgumMetodo) verificam se o objeto já foi descartado. Se afirmativo, eles geram uma exceção.
  

 Outras informações: http://msdn.microsoft.com/pt-br/library/fs2xkftw.aspx