CodeGym /Blogue Java /Random-PT /Referências fantasmas em Java
John Squirrels
Nível 41
San Francisco

Referências fantasmas em Java

Publicado no grupo Random-PT
Oi! Na discussão de hoje, falaremos em detalhes sobre "referências fantasmas" (PhantomReference) em Java. Que tipo de referências são essas? Por que eles são chamados de "referências fantasmas"? Como eles são usados? Como você deve se lembrar, Java tem 4 tipos de referências:
  1. StrongReference (referências comuns que criamos ao criar um objeto):

    Cat cat = new Cat()

    Neste exemplo, cat é uma referência forte.

  2. SoftReference (referência suave). Tivemos uma aula sobre tais referências.

  3. WeakReference (referência fraca). Também houve uma lição sobre eles aqui .

  4. PhantomReference (referência fantasma).

Os três últimos são tipos genéricos com parâmetros de tipo (por exemplo, SoftReference<Integer> , WeakReference<MyClass> ). As classes SoftReference , WeakReference e PhantomReference são herdadas da classe Reference . Os métodos mais importantes ao trabalhar com essas classes são:
  • get() — retorna o objeto referenciado;

  • clear() — remove a referência ao objeto.

Você se lembra desses métodos das lições sobre SoftReference e WeakReference . É importante lembrar que eles funcionam de forma diferente com diferentes tipos de referências. Hoje não vamos nos aprofundar nos três primeiros tipos. Em vez disso, falaremos sobre referências fantasmas. Abordaremos os outros tipos de referências, mas apenas no que diz respeito a como elas diferem das referências fantasmas. Vamos! :) Para começar, por que precisamos de referências fantasmas? Como você sabe, o coletor de lixo (gc) libera a memória usada por objetos Java desnecessários. O coletor exclui um objeto em duas "passagens". Na primeira passagem, ele apenas examina os objetos e, se necessário, os marca como "desnecessários" (ou seja, "a serem excluídos"). Se ofinalize()método foi substituído para o objeto, ele é chamado. Ou talvez não seja chamado - tudo depende apenas se você tiver sorte. Você provavelmente se lembra que finalize()é inconstante :) Na segunda passagem do coletor de lixo, o objeto é excluído e a memória é liberada. O comportamento imprevisível do coletor de lixo cria vários problemas para nós. Não sabemos exatamente quando o coletor de lixo começará a funcionar. Não sabemos se o finalize()método será chamado. Além disso, uma referência forte a um objeto pode ser criada enquanto seu finalize()método está sendo executado, caso em que o objeto não será excluído. Para programas que exigem muito da memória disponível, isso pode levar facilmente a um arquivo OutOfMemoryError. Tudo isso nos leva a usar referências fantasmas. O fato é que isso muda o comportamento do coletor de lixo. Se o objeto tiver apenas referências fantasmas, então:
  • seu método finalize() é chamado (se for substituído)

  • se nada mudar depois que o método finalize() for concluído e o objeto ainda puder ser excluído, a referência fantasma ao objeto será colocada em uma fila especial: ReferenceQueue .

A coisa mais importante a entender ao trabalhar com referências fantasmas é que o objeto não é excluído da memória até que sua referência fantasma esteja nesta fila. Ele será excluído somente depois que o método clear() for chamado na referência fantasma. Vejamos um exemplo. Primeiro, criaremos uma classe de teste que armazenará algum tipo de dado.

public class TestClass {
   private StringBuffer data;
   public TestClass() {
       this.data = new StringBuffer();
       for (long i = 0; i < 50000000; i++) {
           this.data.append('x');
       }
   }
   @Override
   protected void finalize() {
       System.out.println("The finalize method has been called on the TestClass object");
   }
}
Quando criamos objetos, damos a eles uma "carga" pesada intencionalmente (adicionando 50 milhões de caracteres "x" a cada objeto) para ocupar mais memória. Além disso, sobrescrevemos o método finalize() para ver se ele é executado. Em seguida, precisamos de uma classe que herdará de PhantomReference . Por que precisamos de tal classe? É tudo simples. Isso nos permitirá adicionar lógica adicional ao método clear() para verificar se a referência fantasma foi realmente limpa (o que significa que o objeto foi excluído).

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;

public class MyPhantomReference<TestClass> extends PhantomReference<TestClass> {

   public MyPhantomReference(TestClass obj, ReferenceQueue<TestClass> queue) {

       super(obj, queue);

       Thread thread = new QueueReadingThread<TestClass>(queue);

       thread.start();
   }

   public void cleanup() {
       System.out.println("Cleaning up a phantom reference! Removing an object from memory!");
       clear();
   }
}
Em seguida, precisamos de um thread separado que aguardará o coletor de lixo fazer seu trabalho e links fantasmas aparecerão em nosso ReferenceQueue . Assim que tal referência terminar na fila, o método cleanup() é chamado nela:

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;

public class QueueReadingThread<TestClass> extends Thread {

   private ReferenceQueue<TestClass> referenceQueue;

   public QueueReadingThread(ReferenceQueue<TestClass> referenceQueue) {
       this.referenceQueue = referenceQueue;
   }

   @Override
   public void run() {

       System.out.println("The thread monitoring the queue has started!");
       Reference ref = null;

       // Wait until the references appear in the queue
       while ((ref = referenceQueue.poll()) == null) {

           try {
               Thread.sleep(50);
           }

           catch (InterruptedException e) {
               throw new RuntimeException("Thread " + getName() + " was interrupted!");
           }
       }

       // As soon as a phantom reference appears in the queue, clean it up
       ((MyPhantomReference) ref).cleanup();
   }
}
E, finalmente, precisamos do método main() , que colocaremos em uma classe Main separada . Nesse método, criaremos um objeto TestClass , uma referência fantasma a ele e uma fila para referências fantasmas. Depois disso, vamos chamar o coletor de lixo e ver o que acontece :)

import java.lang.ref.*;

public class Main {

   public static void main(String[] args) throws InterruptedException {
       Thread.sleep(10000);

       ReferenceQueue<TestClass> queue = new ReferenceQueue<>();
       Reference ref = new MyPhantomReference<>(new TestClass(), queue);

       System.out.println("ref = " + ref);

       Thread.sleep(5000);

       System.out.println("Collecting garbage!");

       System.gc();
       Thread.sleep(300);

       System.out.println("ref = " + ref);

       Thread.sleep(5000);

       System.out.println("Collecting garbage!");

       System.gc();
   }
}
Saída do console:

ref = MyPhantomReference@4554617c 
The thread monitoring the queue has started! 
Collecting garbage! 
The finalize method has been called on the TestClass object 
ref = MyPhantomReference@4554617c 
Collecting garbage! 
Cleaning up a phantom reference! 
Removing an object from memory! 
O que vemos aqui? Tudo aconteceu como planejamos! O método finalize() do nosso objeto foi substituído e foi chamado durante a execução do coletor de lixo. Em seguida, a referência fantasma foi colocada no ReferenceQueue . Enquanto estava lá, seu método clear() foi chamado (dentro do qual chamamos cleanup() para enviar para o console). Finalmente, o objeto foi excluído da memória. Agora você vê exatamente como isso funciona :) Claro, você não precisa memorizar toda a teoria sobre referências fantasmas. Mas seria bom se você se lembrasse pelo menos dos pontos principais. Primeiro, essas são as referências mais fracas de todas. Eles entram em ação apenas quando não há mais nenhuma referência ao objeto. A lista de referências que fornecemos acima é classificada em ordem decrescente do mais forte para o mais fraco: StrongReference -> SoftReference -> WeakReference -> PhantomReference Uma referência fantasma entra na batalha somente quando não há referências fortes, suaves ou fracas ao nosso objeto: ) Em segundo lugar, o método get() sempre retorna nulo para uma referência fantasma. Aqui está um exemplo simples onde criamos três tipos diferentes de referências para três tipos diferentes de carros:

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;

public class Main {

   public static void main(String[] args) {

       Sedan sedan = new Sedan();
       HybridAuto hybrid = new HybridAuto();
       F1Car f1car = new F1Car();

       SoftReference<Sedan> softReference = new SoftReference<>(sedan);
       System.out.println(softReference.get());

       WeakReference<HybridAuto> weakReference = new WeakReference<>(hybrid);
       System.out.println(weakReference.get());
      
       ReferenceQueue<F1Car> referenceQueue = new ReferenceQueue<>();

       PhantomReference<F1Car> phantomReference = new PhantomReference<>(f1car, referenceQueue);
       System.out.println(phantomReference.get());

   }
}
Saída do console:

Sedan@4554617c
HybridAuto@74a14482 
null
O método get() retornou objetos totalmente comuns para as referências suaves e fracas, mas retornou nulo para a referência fantasma. Em terceiro lugar, as referências fantasmas são usadas principalmente em procedimentos complicados para excluir objetos da memória. É isso! :) Isso conclui nossa lição de hoje. Mas você não pode ir longe apenas na teoria, então é hora de voltar a resolver as tarefas! :)
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION