Java 9 und Jigsaw

Neue Tools in Java 9

Java 9 Modul-Konzept

Die wichtigste Neuerung von Java 9 ist zweifellos das neue Modul-Konzept – auch als „jigsaw“ bezeichnet (JEPs 200, 201, 220, 260, 261, 281).

Das Modulkonzept verfolgt die Ziele „reliable configuration“ und „strong encapsultion“. Was ist damit gemeint?

Im „alten“ Java werden Klassen Pakete zugeordnet. Nur die als „public“ gekennzeichneten Klassen eines Pakets können von Klassen anderer Pakete genutzt werden. Nicht-öffentliche Klassen können nur in anderen Klassen desselben Pakets verwendet werden.

Das Paket war bislang die höchste Organisations-Einheit, die Java kannte. Natürlich konnten class-Dateien zu jar-Dateien zusammengefasst werden – eine jar-Datei war bislang aber eben nichts anderes als ein gezipptes Verzeichnis mit class-Dateien. Eine jar-Datei hatte also kein dem Compiler oder der VM bekannte Java-Beschreibung. Das „Interface“ einer jar war identisch mit ihrem Inhalt.

Daraus ergaben sich Probleme:

  • Angenommen, eine Anwendung enthält Klassen der Pakete jj.mod.pub und jj.mod.pri. In jj.mod.pub existiert die Klasse Foo, in jj.mod.pri existiert die Klasse Bar – eine Helper-Klasse, die von Foo genutzt wird. Bar muss nun public sein – ansonsten könnte sie von Foo nicht genutzt werden. Damit ist Bar natürlich auch für alle anderen Klassen nutzbar – obwohl sie nur als Helper-Klasse für Foo gedacht war. Wobei es völlig unerheblich ist, wie die class-Dateien zu jars zusammengefasst werden.
  • Klassen, die ein und demselben Paket zugeordnet waren, konnten auf verschiedene jar-Dateien aufgeteilt werden. Das trug nicht unbedingt zur Übersichtlichkeit bei.
  • Die Klassen, die zur Compilationszeit und zur Laufzeit herangezogen wurden, wurden über den CLASSPATH ermittelt. Enthielt der CLASSPATH zwei gleichnamige Klassen, so wurde die erste dieser Klassen herangezogen (die Änderung der Reihenfolge der Elemente des CLASSPATH konnte also eine sehr überraschende Wirkung haben).
  • Einer jar-Datei konnte man nicht ansehen, von welchen anderen jars sie abhängig war. Im Grunde war es immer Glücksache, alle benötigten jar-Dateien aufgefunden und bereitgestellt zu haben. Dieses Problem kann zwar durch die Benutzung von Build-Werkzeugen wie Apache Maven oder auch dem OSGi-Framework gelöst werden, aber eben nicht im Rahmen des Java-Standards.

Solche und ähnliche Probleme können vermieden werden, wenn das neue Modul-Konzept verwendet wird.

Ein Modul ist jar-Datei, die in ihrem Wurzelverzeichnis eine module-info.class enthält. Diese Datei wird vom Compiler aufgrund einer module-Info.java-Datei. Diese Datei enthält die Selbstbeschreibung des Moduls. Zu dieser Beschreibung gehören u.a. zwei wichtige Elemente: welche Pakete des Moduls sind von anderen Modulen nutzbar? – und von welchen anderen Modulen ist das Modul abhängig?

Wir greifen auf das obige Szenario zurück. Wir bauen ein Modul mit den Klassen Foo und Bar:

Diese beiden Klasse sollen ein Modul namens jj.mod bilden. Wir schreiben folgende module-info.java:

Das Modul hat einen eigenständigen Java-Namen:  jj.mod. Solche Modul-Namen werden nach demselben Schema gebildet wie Paket-Namen. Der Modul-Name, die Namen der Pakete, die das Modul zusammenfasst, sind technisch völlig unabhängig voneinander – dennoch sollte natürlich ein gewisser Zusammenhang erkennbar sein.

Eine Modul jj.appl, welches dieses Modul nutzt, wird folgende Beschreibungsdatei enthalten:

Die Klassen des jj.appl-Module können nun die Klassen aller Pakete des Moduls jj.mod nutzen, welche von diesem Modul exportiert werden – hier also nur die Klasse Foo:

Module können zudem transitiv exportiert werden; und sie können ihre Pakete nur einer begrenzten Menge anderer Module zur Nutzung zur Verfügung stellen.

Mit diesem neuen Modul-Konzept wird der Modul-Pfad eingeführt, der den CLASSPATH ersetzen kann. Wird statt des CLASSPATH der Modul-Pfad verwendet, wird zudem verhindert, dass Pakete auf mehrere Module verteilt werden können.

Natürlich muss gewährleistet sein, dass auch „alte“ jar-Dateien weiterhin genutzt werden können: sofern eine jar-Datei keine module-info.class enthält, exportiert sie implizit all ihre Pakete – und kann auf alle anderen jar-Dateien (auch auf modulare) zugreifen.

Und umgekehrt kann auch eine alte VM die neuen modularen jars nutzen – alte VMs ignorieren eine Datei namens module-info.class („illegaler“ Klassenname wegen des Bindestrichs).

Auch die rt.jar ist übrigens in mehrere Module zerlegt worden (eine Anwendung muss nur diejenigen Java-SE-Module an Bord haben, die sie tatsächlich benötigt).

Insbesondere bei der Erstellung großer Programmsysteme und bei der Erstellung von Bibliotheken ist das neue Modul-Konzept natürlich von grundlegender Bedeutung.

Publisher-Subscriber

Java 9 führt ein Publisher-Subscriber-Framework ein, welches der Reactive Streams Specification entspricht (java.utilconcurrent.Flow). Reactive Streams sind „asynchronous streams of data with non-blocking back pressure“. Ein reactive stream lässt sich grob als eine Kombination des Iterator- und des Observer-Patterns. Ein Iterator folgt dem Pull-Modell; ein Oberser folgt dem Push-Modell. Beim Flow-API fordert zunächst der Subscriber eine bestimmte Anzahl von zu verarbeitenden Objekten an (pull); dann stellt der Publisher diese Objekte dem Supplier in der von ihm gewünschten Anzahl zu (push). Das von Java 9 eingeführte Framework spezifiziert allerdings ausschließlich Interfaces – Implementierungen dieser Interfaces werden bereitgestellt z.B. von RxJava. (JEP 266)

jshell

Java 9 enthält nun eine jshell, ein interaktives Tool zur Auswertung von Deklarationen, Expressions und Anweisungen. Das Tool implementiert das REPL-Muster (Read-Eval-Print-Loop). Zwei kleine Beispiele:

jshell> Math.sqrt(2)

$1 ==> 1.4142135623730951

 

jshell> {     

   …> int a = 25;     

   …> int b = 15;     

   …> while (a != b) {     

   …> if (a > b) a -= b; else b -= a;     

   …> }     

   …> System.out.println(a);     

   …> }

5

Man kann nun also Java-Fragmente ausprobieren, ohne zunächst den Code komplett übersetzen und zu seiner Ausführung eine VM starten zu müssen (JEP 222).

Collection-Factory-Methods

Java 9 führt einige neue Factory-Methoden ein, mittels derer auf bequeme Weise immutable Collections erzeugt werden können (List.of, Set.of, Map.of, Map.ofEntries).

Erweiterungen der Klasse Process

Die Klasse Process erlaubt nun eine wesentlich erweiterte Kontrolle und Monitoring nativer Prozesse (diese Möglichkeiten waren bislang recht begrenzt). Neu sind in diesem Kontext u.a. die Interfaces ProcessHandle und ProcessHandle.Info.

Stream-API

Das Stream-API wurde erweitert. Neu sind hier insbesondere die Methoden dropWhile und takeWhile.

StackWalker

Informationen über den aktuellen Stack mussten bislang mit der Throwable-Methode getStackTrace abgefragt werden. Diese Methode liefert den kompletten Stack zurück. Mittels der in Java-9 eingeführten Klasse StackWalker können solche Informationen nun lazy ermittelt werden. Somit kann man z.B. erhebliche Kosten sparen, sofern man nur an den obersten Elementen des Stacks interessiert ist (JEP 259)

HTTP/2

Java 9 führt ein neues HTTP client API ein, das HTTP/2 und WebSocket implementiert und ersetzt somit das alte HttpURLConnection-API. Das neue API wird als Incubator-Modul ausgeliefert (es ist also noch offiziell Bestandteil der SE) (JEP 110)

Multi-release jars

Das jar-Dateiformat erlaubt nun die Koexistenz mehrerer Java-Release-spezifischer Versionen von class-Dateien („multi-realease jar-files“). Dieses neue Feature verlangte natürlich auch die Erweiterung des jar-Tools (JEP 238)

GC

Als Garbage-Collector wird nun standardmäßig der G1 verwendet (dieser Collector eignet sich für große Heaps und mimimiert die Häufigkeit von Pausen). Er ersetzt somit den bislang verwendeten Parallel GC (JEP 248).

Compact Strings

Der Speicherverbrauch eines Programms wird maßgeblich von der Speicherung von Strings bestimmt. Strings wurden bislang mittels Arrays von chars implementiert – und ein char umfasst bekanntlich 2 Byte. In den meisten Fällen reicht aber ein einziges Byte zur Speicherung eines Zeichens aus. In Java 9 werden Strings nun – sofern möglich – platzsparender gespeichert („compact strings“) (JEP 254).

Parser-API für JavaScript

Der Java-Script-Interpreter Nashorn wurde um ein Parser-API erweitert. Das API erlaubt die Visitor-basierte Inspektion eines JavaScript-AST-Baumes (JEP 236)

Diamond-Operator

Bislang war die Benutzung des „Diamond-Operators“ beschränkt: bei der Definition von anonymen Klassen konnte er nicht verwendet werden. Diese Beschränkung ist nun aufgehoben worden (auch Kleinigkeiten sind wichtig…).

Für wen hat sich das Warten gelohnt? Für alle!

Was sich Entwickler jetzt noch wünschen? Das Modul-Konzept wird von einigen als zu statisch kritisiert. Kritisiert wird zuweilen auch, dass Module nicht geschachtelt werden können.

 

Seminare zu Java 9:

Neuerungen Java 9 und Jigsaw

Weiterlesen

String deduplication

String Deduplication – ein neues Feature des Garbage Collectors G1

Mit dem Java 7 Update 9 hat Oracle erstmals eine Java Virtual Machine mit dem Garbage Collector G1 („Garbage First“) ausgeliefert. Nach einer langen Testphase soll der G1 zum Standard-Garbage-Collector in Java 9 werden. Durch ein ausgefeiltes Speichermanagement mit kleinen Speicherblöcken verspricht der G1 kürzere Pausenzeiten als die bisher bei den Oracle-VMs verwendeten Garbage-Collectoren.
In der Testphase blieb der G1 zunächst inaktiv und konnte über die VM-Option -XX:+UseG1GC eingeschaltet werden.

String Deduplication ab Java 8 Update 20

Mit dem Java 8 Update 20 wurde der G1 um ein interessantes neues Feature erweitert: Die Deduplizierung von String-Objekten („String Deduplication“). Damit soll der Speicherverbrauch einer Anwendung deutlich reduziert werden, in dem identische Zeichenketten nur einmal gespeichert werden müssen und Duplikate vom G1 entfernt werden.

Um zu verstehen, wie dieses Feature im Detail funktioniert sind vertiefende  Kenntnisse der String-Klasse notwendig. Die String-Klasse benutzt als Datencontainer für eine Zeichenkette ein Array vom Typ char:

private final char value[];

Die Idee der String Deduplication ist, diese Arrays bei gleichem Inhalt zusammenzufassen und auf diese Art Speicherplatz einzusparen. Wie geht der G1 dabei vor?

String Deduplication vorher
String Deduplication vorher

String-Objekte werden zur Laufzeit eines Java-Programms auf dem Heap angelegt. Dabei können Instanzen entstehen, deren value-Arrays gleich lang sind und gleichen Inhalt haben. Um diese Instanzen zu finden, fügt der G1 bei der Garbage Collection String-Instanzen nach einem bestimmten Muster in eine Warteschlange ein, die in einem separaten Thread abgearbeitet werden. In diesem Thread wird für jede enthaltene String-Instanz durch eine Hashtable geprüft, ob bereits ein String mit dem gleichen value-Array bekannt ist. Ist dies der Fall, bekommt das value-Attribut der Instanz den Wert des value-Attributs aus dem bereits in der Hashtable enthaltenen String. Beide String-Instanzen referenzieren damit das selbe Array von char-Werten.

String Deduplication nachher
String Deduplication nachher

Damit kann im Beispiel ein char-Array der Länge 10, also ein Speicherblock von 20 Bytes Größe, wieder freigegeben werden.

Aktivieren der String Deduplication

Wie jede Optimierung ist auch die String Deduplication eine Tradeoff-Optimierung: Zur Durchführung werden zusätzliche Ressourcen eingesetzt. In diesem Fall sind das:

  • Rechenzeit für den deduplizierenden Thread
  • Speicherplatz für die Hashtable, mit der die Kandidaten für die Deduplizierung gefunden werden.

Da sich die Optimierung nicht in jedem Fall lohnt, ist die String Deduplication zunächst deaktiviert. Um sie einzuschalten, kann man beim Start der JVM die Option -XX:+UseStringDeduplication angeben. Um zu ermitteln, welchen Erfolg die String Deduplication bringt, ist die Option -XX:+PrintStringDeduplicationStatistics hilfreich. (Zur Erinnerung: String Deduplication funktioniert nur mit dem Garbage Collector G1. Bis zum Erscheinen von Java 9 ist also zusätzlich die VM-Option -XX:+UseG1GC notwendig.)

Und was ist mit String Interning?

Java besitzt schon immer einen Mechanismus zur Kanonisierung von String-Objekten, nämlich das sogenannte Interning. Dieses Verfahren wird automatisch vom Klassenlader verwendet, um String-Literale zusammenzufassen. Auch der Entwickler könnte ein String-Objekt durch Aufruf der Methode intern() explizit kanonisieren.
Die durch intern() kanonisierten String-Objekte werden aber im sogenannten Runtime Constant Pool gespeichert. Da dieser Speicherbereich bis einschließlich Java 7 recht knapp bemessen war und eine Garbage Collection nicht garantiert war, hat Oracle vor der expliziten Verwendung von intern() durch den Entwickler abgeraten.
Zwei weitere Gründe sprechen gegen das String Interning:

  • Es ist in vielen Anwendungsfällen schwierig zu erkennen, ob String Interning aussichtsreich ist.
  • String Interning nutzt die Rechenzeit des Anwendungsthreads. Das bedeutet: Das Anwendungsprogramm hat mehr zu tun – egal, ob die zusätzlich investierte Arbeit etwas bringt oder nicht.

String Deduplication löst beide Probleme: Der Anwendungsthread wird nicht belastet, da die Deduplizierung in einem weiteren Thread stattfindet. Außerdem werden einfach alle String-Instanzen in Betracht gezogen – der Erfolg hängt also nicht von der Kompetenz des Entwicklers ab.

Fazit String Deduplication

Mit der String Deduplication bringt Java 9 ein spannendes neues Feature, das den Memory-Footprint bei vielen Java-Programmen deutlich reduzieren kann. Die Funktionalität braucht dabei lediglich beim Start der VM aktiviert zu werden; eine Änderung des Anwendungsprogramms ist nicht notwendig.

 

Seminare zu String Deduplication

 

Weiterlesen