Java com café: Garbage Collector - Java

Garbage Collector - Java

Olá! Hoje vou falar sobre um assunto muito interessante: Coleta de lixo. Uma pergunta muito comum quando falamos de coleta de lixo é "How many objects are eligible for the garbage collector?". Como saber quantos objetos são elegíveis a serem descartados? Vamos descobrir como responder esta questão e também aprender sobre um conceito importante conhecido por Island of Isolation ou Ilha de Isolamento. Antes de continuar vamos ver algo sobre atribuições.


Atribuições

Quando atribuímos um objeto a uma variável, o objeto não é armazenado na variável em si, mas sim no heap. O heap é um espaço na memória onde todos os objetos ficam alocados durante a execução de um programa. As variáveis de instância também residem no heap.
Quando atribuímos um primitivo a uma variável, ela realmente armazena o padrão de bits que representa o valor sendo atribuído a ela, mas se tratando de objetos, a variável armazena o endereço do objeto no heap. Por isto esse tipo de variável que faz referência a objetos é chamada de variável de referência. Abaixo temos um exemplo de código e em seguida uma representação visual:
Object objeto1 = new Object();
Object objeto2 = new Object();
Object objeto3 = new Object();

O resultado disto é:

Garbage Collector

O propósito da coleta de lixo é descartar os objetos que não podem mais ser acessados. Isto é uma forma de gerenciar a memória e evitar que os programas fiquem sem espaço para continuar sua execução.
Quando algum objeto no heap não pode mais ser mais alcançado por ninguém (por nenhum thread ativo), ele é elegível a coleta de lixo. Isto pode acontecer de duas formas:

  1. A variável passa a fazer referência a null:
objeto1 = null;

O resultado disto é:


A outra forma é fazer a variável referenciar outro objeto:
objeto1 = objeto2;

O resultado é:

Qualquer uma das formas tornaria o objeto1 qualificado para coleta.
Suponhamos que o objeto1 esteja "solto" no heap. Posso resolver descartá-lo quando quiser? Não. Somente a JVM pode decidir quando isso deve ser feito, mas como bom programador você pode recomendar que a coleta seja feita. E para fazer isto basta usar a instrução:
System.gc();

Não há garantia de que a coleta seja feita, mas geralmente a JVM atende o pedido.
É importante lembrar que um objeto só existirá no heap caso ele tenha sido inicializado (pela keyword new). Portanto a seguinte declaração não cria nenhum objeto no heap:
Object o = null;

Se o objeto nunca foi criado então não existe objeto a ser coletado.


O Método finalize()

A classe Object possui um método chamado finalize(). Como todas classes são filhas de Object, implícita ou explicitamente, todas herdam tal método. Este método é chamado pelo coletor de lixo quando não há mais referências ao objeto. Mas lembre-se que: se não há garantia que a coleta seja executada quando você solicitar, finalize() pode não ser executado.
O método finalize() de Object não executa nenhuma ação, mas pode ser sobrescrito para que faça algo antes que o objeto seja coletado. Pode ser feito uma liberação de recursos, como fechar um arquivo, finalizar uma conexão, etc. Também podemos usá-lo para salvar o objeto da coleta, daqui a pouco explico melhor.
O método finalize() dá ao programador o poder sobre a vida e a morte dos objetos. Para entender melhor essa afirmação, veja dois conceitos importantes sobre o método:

  • finalize() só é chamado uma única e exclusiva vez para qualquer objeto durante a coleta.
  • Chamar finalize() pode salvar o objeto da coleta.

A primeira afirmação significa que se você chamar finalize() por conta própria, ele não será chamado novamente quando a coleta for executada. E a segunda significa que podemos inserir algum código em finalize() para fazer que o objeto tenha uma nova referência e não seja coletado. Mas ressucitar um objeto não é tão simples assim. Abaixo temos um pequeno exemplo de como isto é feito:
public class Objeto{

// Sobrescreve finalize
public void finalize(){
// Da uma nova referência para objeto1
objeto1 = new Objeto();
}

// Objeto que vai ressuscitar
static Objeto objeto1 = new Objeto();

public static void main(String[] args) {

objeto1 = null; // Elegível a coleta

System.gc(); // Chama finalize como consequência

// Aqui faz o thread dormir um pouco
// enquanto a coleta é feita (espero que seja)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

objeto1.go(); // Não deveria causar um NullPointerException?

}

void go(){
System.out.println("Estou vivo!");
}
}

Veja que quando main começar a ser executado, objeto1 faz referência a null, se tornando elegível a coleta. Então é solicitado que a coleta seja feita (reze pra JVM aceitar o pedido). Logo após fazemos o thread main descançar um pouco... Há muitas incertezas neste momento. Desejamos que a coleta seja feita e que isso aconteça enquanto main tira um cochilo. Primeiro não sabemos se a JVM vai nos atender e muito menos se isso será feito num segundo. Mas com sorte tudo vai correr bem. Nos meus testes a JVM respondeu bem e a saída foi:
Estou vivo!

Uma parte importante do código é o cochilo de main. Como main e o coletor de lixo rodam em threads separados, não sabemos quem vai tomar o processador primeiro. (mais sobre threads aqui). Pelo que eu percebi, o coletor sempre fica pra depois e isso é compreensível, porque senão ele poderia prejudicar o desempenho do programa. Se o trecho de código onde main fica em espera for removido, antes que o coletor seja executado o método go() será chamado e causará um NullPonterException. Isso mostra que chamar o coletor não garante que ele seja executado naquele exato momento.
Continuando... Agora vamos praticar um pouco. Veja uma questão que tem grandes chances de aparecer no exame SCJP:
Given:
interface Animal {
void makeNoise();
}

class Horse implements Animal {
Long weight = 1200L;

public void makeNoise() {
System.out.println("whinny");
}
}

public class Icelandic extends Horse {
public void makeNoise() {
System.out.println("vinny");
}

public static void main(String[] args) {
Icelandic i1 = new Icelandic();
Icelandic i2 = new Icelandic();
Icelandic i3 = new Icelandic();
i3 = i1;
i1 = i2;
i2 = null;
i3 = i1;
} //Line 14
}
When line 14 is reached, how many objects are
eligible for the garbage collector?

A. 0
B. 1
C. 2
D. 3
E. 4
F. 6

Analisando o código vemos que existem algumas coisas querendo nos desviar do foco principal: Interface e sobrescrição de métodos. Horse implementa o médodo de Animal corretamente e declara uma variável do tipo Long (Long não é long, isso fará diferença no resultado). Depois Icelandic, que é subclasse de Horse, sobrescreve o método makeNoise corretamente. Tirando essas distrações, entramos na execução do código:
Icelandic i1 = new Icelandic();
Icelandic i2 = new Icelandic();
Icelandic i3 = new Icelandic();

A melhor coisa a se fazer é desenhar o esquema:

Note que cada objeto Icelandic possui uma variável weight, herdada de Horse.
Então continuando no programa temos i3 fazendo referência a i1:
i3 = i1;


Neste momento já temos 2 objetos qualificados pra coleta. Porque 2? Lembra da variável Long? Pois então, Long é uma classe wrapper e portanto é um objeto no heap (variáveis de instância residem do heap). Continuando no progama temos:
i1 = i2;


Aqui não temos muitas mudanças, nenhum objeto é qualificado. i1 faz referência ao objeto de i2, mas o objeto que i1 referenciava ainda é acessível através de i3. Continuando:
i2 = null;


Neste momento i2 passou a referenciar null, mas o objeto que era referenciado por ele anteriormente ainda é referenciado por i1. E finalmente temos:
i3 = i1;


Novamente i3 faz referencia a i1, só que agora i1 faz referência ao objeto que era referenciado por i2. Isso torna o objeto (que antes era referenciado por i1 e depois por i3), qualificado para coleta.
Ufa! Mas enfim quantos objetos se tornaram elegíveis a coleta? Quatro. Dois objetos do tipo Icelandic e dois do tipo Long. Geralmente esse tipo de pergunta inclui essas variáveis de instância que são tipos de classe wrapper só pra confudir.
Um modo de saber quantos objetos foram descartados é sobrescrever finalize() para que mostre alguma mensagem. Veja o código:
public class Icelandic {
Long x;

public void finalize() {
System.out.println("tchau");
}

public static void main(String[] args) {
Icelandic i1 = new Icelandic();
Icelandic i2 = new Icelandic();
Icelandic i3 = new Icelandic();

i3 = i1;
i1 = i2;
i2 = null;
i3 = i1;

System.gc();
}
}

A saída deste código é:
tchau
tchau

Quatro objetos foram coletados. Note que a classe tem uma variável de instância Long.


Ilhas de Isolamento

Ilhas de isolamento ocorrem quando variáveis de instância fazem referência umas as outras formando um circuito fechado. Então se as referência a seus referências forem anuladas, os objetos perderão seu vínculo com elas e terão referências apenas uns dos outros. Analise o código:
public class Icelandic {

// Variável de instância
Icelandic i;

public void finalize() {
System.out.println("tchau");
}

public static void main(String[] args) {

Icelandic i1 = new Icelandic();
Icelandic i2 = new Icelandic();

// Variáveis de instância apontam uma para outra
i1.i = i2;
i2.i = i1;

// As variáveis locais perdem a referência
i1 = null;
i2 = null;

System.gc();
}
}

O cenário inicial deste código é representado abaixo:


Depois das declarações a variavel de instância i de i1 (i1.i) faz referência a i2. E a variável de instância i de i2 (i2.i) faz referência a i1:


Logo depois i1 e i2 passam a referenciar null:


Consegue sentir como é dramático este cenário? Embora os objetos tenham variáveis que fazem referência entre eles, nenhum thread ativo pode alcançá-los. Não importa se eles podem acessar um ao outro, de fora ninguém pode acessá-los. Portanto estão qualificados a coleta de lixo.

Conclusão

Sei que esse assunto é um pouco confuso, mas basta fazer os desenhos pra exergar a solução. Bons estudos e até a próxima!


18 comentários:

  1. kra...
    muto bom o eu post...
    me ajudou bastante...
    forte abraço.

    ResponderExcluir
  2. Marcio,

    excelente post.

    Vai me ajudar muito na certificação.

    ResponderExcluir
  3. Parabéns, ótimo post!

    Realmente o que eu procurava, para sanar minha dúvida sobre "How many objects are eligible for the garbage collector?".

    ResponderExcluir
  4. Bem legal as informações, vai ajudar muito nas questões de concursos que estou estudando! Obrigado o/

    ResponderExcluir
  5. Muito interessante, serviu muito!

    ResponderExcluir
  6. Muito Obrigado...!
    Valeu mesmo...!
    Estudando pra SCJP
    Sanou várias dúvidas
    Por isso cada vez mais apaixonado por JAVA, uma comunidade totalmente ativa, espero daqui uns anos poder contribuir.

    ResponderExcluir
  7. Fiz um teste com esse seu último código, e gostaria de fazer uma pergunta. Quando eu criei mais uma referência para Icelandic, chamada i3, e executei o código, o resultado foi duas mensagens "tchau!". Isso significa que na memória só existem a variável de instância mais a referência de i3?

    ResponderExcluir
    Respostas
    1. Oi Mauro! Se você apenas adicionou mais uma declaração:
      Icelandic i3 = new Icelandic();
      Você tem a variável de instância na memória e ela pode ser acessada através da referência i3. Entendeu?

      Excluir
  8. Muito bom, na questão da SCJP eu não tinha me ligado que o objeto do atributo da classe também ficaria disponivel para o GC ... vc me fez ver isso, vlw

    ResponderExcluir
    Respostas
    1. Isso aí! São objetos e também ficarão disponíveis para o GC.

      Excluir
  9. Tenho uma duvida???
    se eu crio um objeto exemplo

    class Motor
    {

    }

    class Carro
    {

    Motor motor
    }

    quando eu crio um OBJETO
    Carro c = new Carro(),
    e coloco Motor m = new Motor()
    e insiro dentro do objeto carro o motor ex:
    c.motor = m ;

    ate aqui beleza..
    mas se eu fizer c = null ;

    eu sei que se ja nao referencia o objeto Carro criado, e esta elegivel para coleta, porem o motor de dentro do carro se torna null tbm??? e esta elegivel para a coleta???

    ResponderExcluir
    Respostas
    1. Rafael, se o objeto puder ser acessado por uma thread ativa ou referência estática, ele não estará elegível para coleta. No seu caso motor ainda poderá ser acessado por "m", mas ele poderia ser elegível desta forma:
      c.motor = new Motor();

      Excluir
  10. Cara, você não sabe como aprendi com esse post. Parabéns....

    ResponderExcluir
  11. Márcio, parabéns pelo post.
    Estava difícil encontrar uma explicação esclarecedora sobre ilha de isolamento.
    Abraço

    ResponderExcluir
    Respostas
    1. Romero, obrigado pelo comentário. Sempre tendo abordar o assunto da maneira mais clara possível.

      Excluir
  12. Fiquei com uma dúvida. Você disse "É importante lembrar que um objeto só existirá no heap caso ele tenha sido inicializado (pela keyword new)." Porém no trecho abaixo você não instanciou x então ele não seria elegível, correto ? Seriam somente 2 objetos coletados.

    public class Icelandic {
    Long x;

    ResponderExcluir
    Respostas
    1. Ele seria elegível porque é do tipo Long, que é uma classe wrapper e portanto um objeto no heap (variáveis de instância residem do heap).

      Excluir