CodeGym/Blog Java/Random-ES/Referencias fantasma en Java
Autor
Milan Vucic
Programming Tutor at Codementor.io

Referencias fantasma en Java

Publicado en el grupo Random-ES
¡Hola! En la discusión de hoy, hablaremos en detalle sobre las "referencias fantasma" (PhantomReference) en Java. ¿Qué tipo de referencias son estas? ¿Por qué se llaman "referencias fantasma"? ¿Cómo se usan? Como recordará, Java tiene 4 tipos de referencias:
  1. StrongReference (referencias ordinarias que creamos al crear un objeto):

    Cat cat = new Cat()

    En este ejemplo, gato es una referencia fuerte.

  2. SoftReference (referencia suave). Tuvimos una lección sobre tales referencias.

  3. WeakReference (referencia débil). También hubo una lección sobre ellos aquí .

  4. PhantomReference (referencia fantasma).

Los últimos tres son tipos genéricos con parámetros de tipo (por ejemplo, SoftReference<Integer> , WeakReference<MyClass> ). Las clases SoftReference , WeakReference y PhantomReference se heredan de la clase Reference . Los métodos más importantes a la hora de trabajar con estas clases son:
  • get() : devuelve el objeto al que se hace referencia;

  • clear() — elimina la referencia al objeto.

Recuerda estos métodos de las lecciones sobre SoftReference y WeakReference . Es importante recordar que funcionan de manera diferente con diferentes tipos de referencias. Hoy no nos sumergiremos en los primeros tres tipos. En su lugar, hablaremos de referencias fantasma. Nos referiremos a los otros tipos de referencias, pero solo con respecto a cómo se diferencian de las referencias fantasma. ¡Vamos! :) Para empezar, ¿por qué necesitamos referencias fantasma? Como sabe, el recolector de basura (gc) libera la memoria utilizada por objetos Java innecesarios. El colector elimina un objeto en dos "pasos". En la primera pasada, solo mira los objetos y, si es necesario, los marca como "innecesarios" (es decir, "para eliminar"). Si elfinalize()El método ha sido anulado para el objeto, se llama. O tal vez no se llame, todo depende solo de si tienes suerte. Probablemente recuerdes que finalize()es inconstante :) En el segundo paso del recolector de basura, el objeto se elimina y la memoria se libera. El comportamiento impredecible del recolector de basura nos crea una serie de problemas. No sabemos exactamente cuándo comenzará a ejecutarse el recolector de basura. No sabemos si finalize()se llamará al método. Además, se puede crear una fuerte referencia a un objeto mientras finalize()se ejecuta su método, en cuyo caso el objeto no se eliminará en absoluto. Para los programas que exigen mucho de la memoria disponible, esto puede conducir fácilmente a un archivo OutOfMemoryError. Todo esto nos empuja a usar referencias fantasma.. El hecho es que esto cambia el comportamiento del recolector de basura. Si el objeto solo tiene referencias fantasma, entonces:
  • se llama a su método finalize() (si se anula)

  • si nada cambia una vez que finaliza el método finalize() y aún se puede eliminar el objeto, entonces la referencia fantasma al objeto se coloca en una cola especial: ReferenceQueue .

Lo más importante que debe comprender cuando se trabaja con referencias fantasma es que el objeto no se elimina de la memoria hasta que su referencia fantasma esté en esta cola. Se eliminará solo después de que se llame al método clear() en la referencia fantasma. Veamos un ejemplo. Primero, crearemos una clase de prueba que almacenará algún tipo de datos.
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");
   }
}
Cuando creamos objetos, intencionalmente les daremos una gran "carga" (agregando 50 millones de caracteres "x" a cada objeto) para ocupar más memoria. Además, anulamos el método finalize() para ver que se ejecute. A continuación, necesitamos una clase que heredará de PhantomReference . ¿Por qué necesitamos tal clase? Todo es sencillo. Esto nos permitirá agregar lógica adicional al método clear() para verificar que la referencia fantasma realmente se borre (lo que significa que el objeto se eliminó).
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();
   }
}
A continuación, necesitamos un subproceso separado que esperará a que el recolector de basura haga su trabajo, y aparecerán enlaces fantasma en nuestro ReferenceQueue . Tan pronto como dicha referencia termina en la cola, se llama al método cleanup() :
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();
   }
}
Y finalmente, necesitamos el método main() , que lo colocaremos en una clase principal separada . En ese método, crearemos un objeto TestClass , una referencia fantasma a él y una cola para referencias fantasma. Después de eso, llamaremos al recolector de basura y veremos qué sucede :)
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();
   }
}
Salida de la consola:
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!
¿Qué vemos aquí? ¡Todo sucedió como lo planeamos! El método finalize() de nuestro objeto se anula y se invocó mientras se ejecutaba el recolector de elementos no utilizados. A continuación, la referencia fantasma se colocó en ReferenceQueue . Mientras estaba allí, se llamó a su método clear() (dentro del cual llamamos cleanup() para enviar a la consola). Finalmente, el objeto fue borrado de la memoria. Ahora ves exactamente cómo funciona esto :) Por supuesto, no necesitas memorizar toda la teoría sobre las referencias fantasma. Pero sería bueno que recordaras al menos los puntos principales. Primero, estas son las referencias más débiles de todas. Entran en juego solo cuando no quedan otras referencias al objeto. La lista de referencias que proporcionamos anteriormente está ordenada en orden descendente de la más fuerte a la más débil: StrongReference -> SoftReference -> WeakReference -> PhantomReference Una referencia fantasma entra en la batalla solo cuando no hay referencias fuertes, blandas o débiles a nuestro objeto: ) En segundo lugar, el método get() siempre devuelve nulo para una referencia fantasma. Aquí hay un ejemplo simple en el que creamos tres tipos diferentes de referencias para tres tipos diferentes de automóviles:
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());

   }
}
Salida de la consola:
Sedan@4554617c
HybridAuto@74a14482
null
El método get() devolvió objetos completamente ordinarios para las referencias blandas y débiles, pero devolvió un valor nulo para la referencia fantasma. En tercer lugar, las referencias fantasma se utilizan principalmente en procedimientos complicados para eliminar objetos de la memoria. ¡Eso es todo! :) Eso concluye nuestra lección de hoy. Pero no puedes ir muy lejos solo con la teoría, ¡así que es hora de volver a resolver tareas! :)
Comentarios
  • Populares
  • Nuevas
  • Antiguas
Debes iniciar sesión para dejar un comentario
Esta página aún no tiene comentarios