Kolumne

Aus der Java-Trickkiste: Referenzen mit Sonderstatus

Arno Haase

© Shutterstock/Victor Correia

Dass es Weak, Soft und Phantomreferenzen gibt, ist inzwischen recht allgemein bekannt. Und man kann mit ihnen sogar praktische Probleme lösen – ein guter Grund, sie näher unter die Lupe zu nehmen. Die drei speziellen Referenztypen haben gemeinsam, dass sie von der Garbage Collection besonders behandelt werden. Das ist im C++-Quelltext der JVM fest implementiert, man kann also nicht einfach neue Arten von Referenzen mit Sonderbehandlung einführen.

WeakReference

Eine WeakReference speichert eine Referenz auf ein Objekt, ohne dessen Garbage Collection zu verhindern. Eine „normale“ Referenz auf ein Objekt verhindert, dass es abgeräumt wird, WeakReferences nicht. Wenn man eine WeakReference erzeugt, muss man ihr im Konstruktor das referenzierte Objekt übergeben (Listing 1). Der Code dort ruft anschließend System.gc() auf, um eine Garbage Collection auszulösen. Das ist in produktivem Code eine schlechte Idee, weil es fast immer die Performance ruiniert, aber zur Illustration ist es in diesem Beispiel nützlich. Das Objekt in der WeakReference wird nirgendwo sonst referenziert, ist also sofort zur GC freigegeben. Die JVM löscht dann den Inhalt der WeakReference, und zwar bevor er tatsächlich von der GC beseitigt wird.
Um den zeitlichen Ablauf zu illustrieren, legt der Beispielcode ein Objekt vom Typ WithFinalize (Listing 2) in die WeakReference. Diese Klasse überschreibt die finalize()-Methode und gibt dort eine Meldung aus. Es ist in produktivem Code eine schlechte Idee, finalize() zu überschreiben, unter anderem weil es wichtige Hotspot-Optimierungen wie Inlining und Escape Analysis verhindert. Aber wie schon das explizite Auslösen der GC hilft auch diese Sünde im Beispielcode, das Laufzeitverhalten zu demonstrieren.

Reference<WithFinalize> ref = new WeakReference&l>(new WithFinalize());
while(ref.get() != null) System.gc();
System.out.println("Referenz gelöscht");
Thread.sleep(1000);
class WithFinalize {
  @Override public void finalize() {
    System.out.println("finalize");
  }
}

Das Warten von einer Sekunde in der letzten Zeile von Listing 1 verhindert, dass die JVM sich beendet, bevor die GC mit ihrer Arbeit fertig ist.

ReferenceQueue

Oft will man Dinge aufräumen, wenn der Inhalt einer WeakReference verschwunden ist, z. B. indem man sie selbst zur GC freigibt. Und wenn man mit mehreren WeakReferences arbeitet, wird es leicht unübersichtlich und ineffizient, immer wieder jede einzelne Referenz zu überprüfen. Deshalb gibt es die Klasse ReferenceQueue. Man kann eine solche Queue optional als zweiten Parameter im Konstruktor einer WeakReference angeben (Listing 3). Man kann dieselbe Queue für beliebig viele Referenzen verwenden, und wenn eine dieser Referenzen freigegeben wird, erscheint sie einmalig als Eintrag in der Queue.
Wenn man dann wissen will, ob bzw. welche Referenzen seit der letzten Abfrage freigegeben wurden, kann man queue.poll() oder queue.remove() aufrufen – Ersteres kehrt sofort zurück, Letzteres blockiert mit einem optionalen Timeout. Man hat dadurch eine einzige Stelle, bei der man effizient über jede freigegebene Referenz informiert wird.

final ReferenceQueue<WithFinalize> queue = new ReferenceQueue<>();
final Reference<WithFinalize> ref = 
  new WeakReference<>(new WithFinalize(), queue);

while(ref.get() != null) {
  System.gc();
  final Reference<?> r = queue.remove(100);
  if(r != null) {
    System.out.println("from queue: " + r + " -> " + r.get());
  }
}
Thread.sleep(1000);

Die Standardbibliothek enthält eine Klasse WeakHashMap, die ihre Keys in WeakReferences speichert. Sie ist ein niederschwelliger Startpunkt in die Welt der WeakReferences. WeakReferences sind z. B. nützlich, um eine Liste von registrierten Listenern zu speichern. Es passiert leicht, dass ein Stück Code schon lange nicht mehr relevant ist und nur noch von einem Listener am Leben gehalten wird, der darauf verweist. Wenn man Listener in WeakReferences speichert, kann die GC in solchen Fällen ihre Arbeit tun, und man vermeidet Memory Leaks.

PhantomReference

PhantomReferences funktionieren ähnlich wie WeakReferences. Der einzige Unterschied ist, dass die Referenz erst in der ReferenceQueue landet, nachdem das referenzierte Objekt von der GC weggeräumt wurde. Ein typisches Beispiel für ihre Verwendung ist, temporäre Dateien im Dateisystem zu löschen, wenn sie nicht mehr referenziert werden (Listing 4). Als Erstes brauchen wir dazu eine Klasse FileRef, die von PhantomReference erbt und sich den Pfad einer Datei merkt. Die eigentliche TempFileDeleter-Klasse hat eine ReferenceQueue und ein threadsicheres Set mit FileRef-Instanzen.

Die Methode register() registriert ein File-Objekt. Sie erzeugt eine entsprechende FileRef-Instanz, die sich bei der ReferenceQueue registriert. Das neu erzeugte Objekt landet dann noch im Set, um es vor der GC zu schützen. Die Methode cleanup() erledigt die Aufräumarbeit. Sie holt sich der Reihe nach die Referenzen, die in der ReferenceQueue gelandet sind – also diejenigen, deren File-Instanzen der GC anheim gefallen sind. Die ReferenceQueue gehört dem TempFileDeleter, und der speichert dort nur FileRef-Instanzen. Deshalb sind alle Referenzen, die die Queue liefert, FileRefs, und ein entsprechender Typ-Cast funktioniert. Jetzt kann die eigentliche Arbeit passieren: Die Datei, deren Pfad in der FileRef gespeichert war, wird gelöscht. Dabei ist es wichtig, dass nur der Pfad als String und nicht etwa das File selbst in der FileRef gespeichert ist. Das würde nämlich verhindern, dass das File Garbage collected wird. Schließlich wird die FileRef noch aus dem Set gelöscht, weil sie ihren Zweck erfüllt hat.

class TempFileDeleter {
  private static class FileRef extends PhantomReference<File> {
    private final String path;
    public FileRef(File f, ReferenceQueue<? super File> q) {
      super(f, q);
      path = f.getPath();
    }
    private String getPath() { return path; }
  }

  private final ReferenceQueue<File> queue = new ReferenceQueue<>();
  private final Set<FileRef> refs = 
    Collections.newSetFromMap(new ConcurrentHashMap<FileRef, Boolean>());

  public void register(File f) {
    cleanup();
    refs.add(new FileRef(f, queue));
  }

  public void cleanup() {
    while (true) {
      final FileRef ref = (FileRef) queue.poll();
      if(ref == null) {
        return;
      }
      new File(ref.getPath()).delete();
      refs.remove(ref);
    }
  }
}

Diese Art von Aufräumen ist kein Ersatz für sauberes Ressourcen-Handling. Es gibt keine Garantie, wann eine Phantomreferenz freigegeben wird und ob das überhaupt passiert, bevor die JVM terminiert. Für das Löschen von temporären Dateien funktioniert es deshalb, weil das Temp-Verzeichnis ohnehin früher oder später aufgeräumt wird und das Löschen der Dateien deshalb nett, aber nicht essenziell ist. Ein anderer typischer Anwendungszweck ist das Logging von Warnungen, wenn eine Datenbank-Connection nicht in den Collection-Pool zurückgelegt wird.

SoftReference

SoftReferences schließlich sind die dritte Art von Referenzen mit Sonderbehandlung in der JVM. Sie geben ihren Inhalt zu irgendeinem Zeitpunkt frei. Die einzige Garantie ist, dass das passiert, bevor es einen OutOfMemoryError wegen mangelndem Heap gibt.
Die Hotspot-JVM hat die Kommandozeilenoption -XX:SoftRefLRUPolicyMSPerMB=…, die dieses Verhalten steuert: Sie erwartet eine Zahl, die die Lebensdauer von SoftReferences in Millisekunden je freiem Megabyte Heap-Speicher der JVM setzt.
SoftReferences sind für globale Caches gedacht, und Swing benutzt sie z. B. für seinen Font-Cache. Meiner Meinung nach sind sie weitgehend nutzlos – ich finde explizites, gesteuertes Caching nützlicher als einen globalen Cache, den man sich mit Bibliotheken teilt.

Fazit

Die speziellen Referenztypen von Java sind sicher kein Feature, das man jeden Tag braucht. Sie sind aber auch kein Hexenwerk, und wenn man sie einmal verstanden hat, kann man mit ihnen Systeme robuster machen.

Aufmacherbild: Construction site crane building a blue Java 3D text. Part of a series. von Shutterstock / Urheberrecht: Victor Correia

Geschrieben von
Arno Haase
Arno Haase
Arno Haase ist freiberuflicher Softwareentwickler. Er programmiert Java aus Leidenschaft, arbeitet aber auch als Architekt, Coach und Berater. Seine Schwerpunkte sind modellgetriebene Softwareentwicklung, Persistenzlösungen mit oder ohne relationaler Datenbank und nebenläufige und verteilte Systeme. Arno spricht regelmäßig auf Konferenzen und ist Autor von Fachartikeln und Büchern. Er lebt mit seiner Frau und seinen drei Kindern in Braunschweig.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: