CodeGym /Java Blog /Random-IT /Riferimenti fantasma in Java
John Squirrels
Livello 41
San Francisco

Riferimenti fantasma in Java

Pubblicato nel gruppo Random-IT
CIAO! Nella discussione di oggi, parleremo in dettaglio di "riferimenti fantasma" (PhantomReference) in Java. Che tipo di riferimenti sono questi? Perché sono chiamati "riferimenti fantasma"? Come vengono utilizzati? Come ricorderai, Java ha 4 tipi di riferimenti:
  1. StrongReference (riferimenti ordinari che creiamo durante la creazione di un oggetto):

    Cat cat = new Cat()

    In questo esempio, cat è un forte riferimento.

  2. SoftReference (riferimento software). Abbiamo avuto una lezione su tali riferimenti.

  3. WeakReference (riferimento debole). C'era anche una lezione su di loro qui .

  4. PhantomReference (riferimento fantasma).

Gli ultimi tre sono tipi generici con parametri di tipo (ad esempio, SoftReference<Integer> , WeakReference<MyClass> ). Le classi SoftReference , WeakReference e PhantomReference vengono ereditate dalla classe Reference . I metodi più importanti quando si lavora con queste classi sono:
  • get() — restituisce l'oggetto referenziato;

  • clear() — rimuove il riferimento all'oggetto.

Ricordi questi metodi dalle lezioni su SoftReference e WeakReference . È importante ricordare che funzionano in modo diverso con diversi tipi di riferimenti. Oggi non ci addentreremo nei primi tre tipi. Parleremo invece di riferimenti fantasma. Toccheremo gli altri tipi di riferimenti, ma solo rispetto a come differiscono dai riferimenti fantasma. Andiamo! :) Per cominciare, perché abbiamo bisogno di riferimenti fantasma? Come sai, il Garbage Collector (gc) rilascia la memoria utilizzata da oggetti Java non necessari. Il raccoglitore elimina un oggetto in due "passaggi". Nel primo passaggio, esamina solo gli oggetti e, se necessario, li contrassegna come "non necessari" (che significa "da eliminare"). Se lafinalize()metodo è stato sovrascritto per l'oggetto, viene chiamato. O forse non si chiama - tutto dipende solo se sei fortunato. Probabilmente ricorderai che finalize()è volubile :) Nel secondo passaggio del Garbage Collector, l'oggetto viene eliminato e la memoria viene liberata. Il comportamento imprevedibile del Garbage Collector crea una serie di problemi per noi. Non sappiamo esattamente quando inizierà a funzionare il Garbage Collector. Non sappiamo se il finalize()metodo verrà chiamato. Inoltre, è possibile creare un forte riferimento a un oggetto durante finalize()l'esecuzione del suo metodo, nel qual caso l'oggetto non verrà affatto eliminato. Per i programmi che fanno grandi richieste sulla memoria disponibile, questo può facilmente portare a un file OutOfMemoryError. Tutto questo ci spinge a utilizzare riferimenti fantasma. Il fatto è che questo cambia il comportamento del Garbage Collector. Se l'oggetto ha solo riferimenti fantasma, allora:
  • viene chiamato il suo metodo finalize() (se viene sovrascritto)

  • se non cambia nulla una volta terminato il metodo finalize() e l'oggetto può ancora essere eliminato, allora il riferimento fantasma all'oggetto viene inserito in una coda speciale: ReferenceQueue .

La cosa più importante da capire quando si lavora con i riferimenti fantasma è che l'oggetto non viene cancellato dalla memoria finché il suo riferimento fantasma non si trova in questa coda. Verrà eliminato solo dopo che il metodo clear() è stato chiamato sul riferimento fantasma. Diamo un'occhiata a un esempio. Innanzitutto, creeremo una classe di test che memorizzerà alcuni tipi di dati.

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 creiamo oggetti, diamo loro intenzionalmente un pesante "carico" (aggiungendo 50 milioni di caratteri "x" a ciascun oggetto) per occupare più memoria. Inoltre, sovrascriviamo il metodo finalize() per verificare che venga eseguito. Successivamente, abbiamo bisogno di una classe che erediterà da PhantomReference . Perché abbiamo bisogno di una classe del genere? È tutto semplice. Questo ci consentirà di aggiungere ulteriore logica al metodo clear() per verificare che il riferimento fantasma sia realmente cancellato (il che significa che l'oggetto è stato cancellato).

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();
   }
}
Successivamente, abbiamo bisogno di un thread separato che attenderà che il Garbage Collector svolga il suo lavoro e i collegamenti fantasma appariranno nel nostro ReferenceQueue . Non appena tale riferimento finisce nella coda, viene chiamato il metodo cleanup() su di esso:

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();
   }
}
Infine, abbiamo bisogno del metodo main() , che inseriremo in una classe Main separata . In quel metodo, creeremo un oggetto TestClass , un riferimento fantasma ad esso e una coda per i riferimenti fantasma. Successivamente, chiameremo il Garbage Collector e vedremo cosa succede :)

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();
   }
}
Uscita 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! 
Cosa vediamo qui? Tutto è successo come avevamo programmato! Il metodo finalize() del nostro oggetto viene sovrascritto ed è stato chiamato mentre era in esecuzione il Garbage Collector. Successivamente, il riferimento fantasma è stato inserito in ReferenceQueue . Mentre era lì, è stato chiamato il suo metodo clear() (all'interno del quale abbiamo chiamato cleanup() per inviare l'output alla console). Infine, l'oggetto è stato cancellato dalla memoria. Ora vedi esattamente come funziona :) Naturalmente, non è necessario memorizzare tutta la teoria sui riferimenti fantasma. Ma sarebbe bello se ricordassi almeno i punti principali. Innanzitutto, questi sono i riferimenti più deboli di tutti. Entrano in gioco solo quando non rimangono altri riferimenti all'oggetto. L'elenco dei riferimenti che abbiamo fornito sopra è ordinato in ordine decrescente dal più forte al più debole: StrongReference -> SoftReference -> WeakReference -> PhantomReference Un riferimento fantasma entra in battaglia solo quando non ci sono riferimenti forti, morbidi o deboli al nostro oggetto: ) In secondo luogo, il metodo get() restituisce sempre null per un riferimento fantasma. Ecco un semplice esempio in cui creiamo tre diversi tipi di riferimenti per tre diversi tipi di auto:

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());

   }
}
Uscita console:

Sedan@4554617c
HybridAuto@74a14482 
null
Il metodo get() ha restituito oggetti del tutto ordinari per i riferimenti soft e weak, ma ha restituito null per il riferimento fantasma. In terzo luogo, i riferimenti fantasma vengono utilizzati principalmente in complicate procedure per eliminare oggetti dalla memoria. Questo è tutto! :) Questo conclude la nostra lezione di oggi. Ma non puoi andare lontano solo con la teoria, quindi è ora di tornare a risolvere i compiti! :)
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION