Es reicht aus, dass in Java Logger zum Zeitpunkt der Initialisierung der Klasse initialisiert werden. Warum verunreinigen sie den gesamten Start? John Rose zur Rettung!
So könnte es aussehen:
lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
Dieses Dokument erweitert das Verhalten der endgültigen Variablen, sodass Sie optional die verzögerte Ausführung unterstützen können - sowohl in der Sprache selbst als auch in der JVM. Es wird vorgeschlagen, das Verhalten der vorhandenen Mechanismen des Lazy Computing durch Ändern der Granularität zu verbessern: Jetzt ist es nicht mehr klassengenau, sondern auf eine bestimmte Variable genau.

Motivation
Java hat tief in Lazy Computing eingebaut. Fast jede Verbindungsoperation kann faulen Code ziehen. Beispiel: Ausführen der Methode <clinit>
(Bytecode des Klasseninitialisierers) oder Verwenden der Bootstrap-Methode (für eine aufgerufene dynamische Aufrufsite oder CONSTANT_Dynamic
Konstanten).
Klasseninitialisierer sind im Vergleich zu Mechanismen, die Bootstrap-Methoden verwenden, in Bezug auf die Granularität sehr unhöflich, da ihr Vertrag darin besteht, den gesamten Initialisierungscode für die Klasse als Ganzes auszuführen, anstatt sich auf die Initialisierung zu beschränken, die sich auf ein bestimmtes Feld der Klasse bezieht. Die Auswirkungen einer solchen Rohinitialisierung sind schwer vorherzusagen. Es ist schwierig, die Nebenwirkungen der Verwendung eines statischen Felds einer Klasse zu isolieren, da die Berechnung eines Feldes zur Berechnung aller statischen Felder dieser Klasse führt.
Wenn Sie ein Feld berühren, wirken sich diese auf alle aus. In AOT-Compilern ist es daher besonders schwierig, statische Feldreferenzen zu optimieren, selbst für Felder mit einem leicht zu analysierenden konstanten Wert. Sobald mindestens ein neu gestaltetes statisches Feld zwischen den Feldern überfüllt ist, ist es unmöglich, alle Felder dieser Klasse vollständig zu analysieren. Ein ähnliches Problem zeigt sich bei den zuvor vorgeschlagenen Mechanismen zur Implementierung der Faltung von Konstanten (während des Javac- Betriebs) für konstante Felder mit komplexen Initialisierern.
Ein Beispiel für eine neu gestaltete Feldinitialisierung, die in verschiedenen Projekten bei jedem Schritt in jeder Datei erfolgt, ist die Initialisierung des Loggers.
private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
Diese harmlos aussehende Initialisierung startet unter der Haube eine enorme Menge an Arbeit, die während der Initialisierung der Klasse ausgeführt wird - und dennoch ist es äußerst unwahrscheinlich, dass der Logger zum Zeitpunkt der Initialisierung der Klasse wirklich benötigt wird oder möglicherweise überhaupt nicht benötigt wird. Die Möglichkeit, die Erstellung bis zur ersten tatsächlichen Verwendung zu verschieben, vereinfacht die Initialisierung und hilft in einigen Fällen, diese Initialisierung insgesamt zu vermeiden.
Endvariablen sind sehr nützlich, sie sind der Hauptmechanismus der Java-API, um die Konstanz von Werten anzuzeigen. Lazy Variablen funktionierten auch gut. Ab Java 7 spielten sie eine immer wichtigere Rolle in den Interna des JDK und wurden mit der Annotation @Stable
. JIT kann sowohl endgültige als auch stabile Variablen optimieren - viel besser als nur einige Variablen. Durch das Hinzufügen von faulen Endvariablen wird dieses nützliche Verwendungsmuster häufiger, sodass es an mehreren Stellen verwendet werden kann. Durch die Verwendung von Lazy Final-Variablen können Bibliotheken wie das JDK die Abhängigkeit von <clinit>
-Code verringern, was wiederum die Startzeit verkürzen und die Qualität der AOT-Optimierungen verbessern sollte.
Beschreibung
Das Feld kann mit dem neuen Modifikator " lazy
deklariert werden. Hierbei handelt es sich um ein Kontextschlüsselwort, das ausschließlich als Modifikator wahrgenommen wird. Ein solches Feld wird als Lazy Field bezeichnet und muss auch static
und final
Modifikatoren enthalten.
Ein Lazy Field muss einen Initialisierer haben. Der Compiler und die Laufzeit vereinbaren, den Initialisierer genau dann zu starten, wenn die Variable zum ersten Mal verwendet wird, und nicht, wenn die Klasse initialisiert wird, zu der dieses Feld gehört.
Jedes lazy static final
Endfeld wird zur Kompilierungszeit einem konstanten Poolelement zugeordnet, das seinen Wert darstellt. Da die Elemente des konstanten Pools selbst träge berechnet werden, reicht es aus, einfach den richtigen Wert für jede statische träge Endvariable zuzuweisen, die diesem Element zugeordnet ist. (Sie können mehr als eine Lazy-Variable an ein Element binden, dies ist jedoch kaum eine nützliche oder aussagekräftige Funktion.) Der Attributname lautet LazyValue
und muss sich auf ein konstantes Gender-Element beziehen, das ldc-codiert werden kann und in einen Lazy-Field-Typ konvertierbar ist . Es sind nur Casts MethodHandle.invoke
, die bereits in MethodHandle.invoke
.
Daher kann ein verzögertes statisches Feld als benannter Alias für ein konstantes Poolelement innerhalb der Klasse betrachtet werden, die dieses Feld deklariert hat. Tools wie Compiler versuchen möglicherweise irgendwie, dieses Feld zu verwenden.
Ein Lazy Field ist niemals eine konstante Variable (im Sinne von JLS 4.12.4) und wird ausdrücklich von der Teilnahme an konstanten Ausdrücken ausgeschlossen (im Sinne von JLS 15.28). Daher wird das ConstantValue
Attribut niemals erfasst, selbst wenn sein Initialisierer ein konstanter Ausdruck ist. Stattdessen erfasst das Lazy-Feld eine neue Art von Klassendatei-Attribut namens LazyValue
, das die JVM beim Verknüpfen mit diesem bestimmten Feld konsultiert. Das Format dieses neuen Attributs ähnelt dem vorherigen, da es auch auf ein Element des konstanten Pools verweist, in diesem Fall auf das Element, das in den Feldwert aufgelöst wird.
Wenn ein verzögertes statisches Feld verknüpft ist, sollte der normale Prozess der Ausführung von Klasseninitialisierern nicht verschwinden. Stattdessen wird jede deklarierende Klasse <clinit>
-Methode gemäß den in JVMS 5.5 definierten Regeln initialisiert. Mit anderen Worten, der getstatic
Bytecode für ein verzögertes statisches Feld führt die gleiche Verknüpfung aus wie für jedes statische Feld. Nach der Initialisierung (oder während der bereits gestarteten Initialisierung des aktuellen Threads) löst die JVM die dem Feld zugeordneten konstanten Poolelemente auf und speichert die aus dem konstanten Pool erhaltenen Werte in diesem Feld.
Da Lazy Static Final nicht leer sein kann, können ihnen keine Werte zugewiesen werden - auch nicht in den wenigen Kontexten, in denen dies für leere Finalvariablen funktioniert.
Während der Kompilierung werden alle verzögerten statischen Felder unabhängig von nicht verzögerten statischen Feldern initialisiert, unabhängig von ihrer Position im Quellcode. Daher gelten Einschränkungen für die Position statischer Felder nicht für verzögerte statische Felder. Der verzögerte statische Feldinitialisierer kann jedes statische Feld derselben Klasse verwenden, unabhängig von der Reihenfolge, in der sie in der Quelle angezeigt werden. Der Initialisierer eines nicht statischen Felds oder der Klasseninitialisierer kann auf das Lazy-Feld zugreifen, unabhängig davon, in welcher Reihenfolge in der Quelle sie relativ zueinander sind. Normalerweise ist dies nicht die sinnvollste Idee, da die gesamte Bedeutung von Lazy-Werten verloren geht, sie kann jedoch möglicherweise in bedingten Ausdrücken oder im Kontrollfluss verwendet werden. Faule statische Felder können daher eher wie Felder einer anderen Klasse behandelt werden - in dem Sinne, dass sie von jedem Teil der Klasse, in der sie deklariert sind, in beliebiger Reihenfolge referenziert werden können.
Lazy Fields können mithilfe der Reflection-API mithilfe von zwei neuen API-Methoden in java.lang.reflect.Field
erkannt werden. Die neue isLazy
Methode gibt true
dann true
zurück true
wenn das Feld einen lazy
Modifikator enthält. Die neue isAssigned
Methode gibt nur dann false
wenn das Feld faul ist und zum Zeitpunkt des isAssigned
noch nicht initialisiert isAssigned
. (Abhängig vom Vorhandensein von Rennen kann es fast beim nächsten Aufruf im selben Thread true zurückgeben.) Es gibt keine andere Möglichkeit, herauszufinden, ob ein Feld initialisiert ist, als isAssigned
.
(Der Aufruf von isAssigned
wird isAssigned
benötigt, um bei seltenen Problemen im Zusammenhang mit der Lösung zirkulärer Abhängigkeiten zu helfen. Vielleicht können wir auf die Implementierung dieser Methode verzichten. Allerdings möchten isAssigned
, die Code mit faulen Variablen schreiben, dies manchmal wissen ob der Wert auf eine solche Variable gesetzt ist oder noch nicht, ähnlich wie Mutex-Benutzer manchmal anhand des Mutex herausfinden möchten, ob er gesperrt ist oder nicht, aber sie möchten nicht wirklich gesperrt werden.
Es gibt eine ungewöhnliche Einschränkung für verzögerte Endfelder: Sie sollten niemals auf ihre Standardwerte initialisiert werden. Das heißt, das verzögerte Referenzfeld sollte nicht auf null
initialisiert werden, und numerische Typen sollten keinen Nullwert haben. Ein fauler boolescher Wert kann mit nur einem Wert initialisiert werden - true
, da false
der Standardwert ist. Wenn der Initialisierer eines verzögerten statischen Felds seinen Standardwert zurückgibt, schlägt die Verknüpfung dieses Felds mit dem entsprechenden Fehler fehl.
Diese Einschränkung wird dafür eingeführt. Damit JVM-Implementierungen Standardwerte als internen Watchdog-Wert reservieren können, der den Status eines nicht initialisierten Felds kennzeichnet. Der Standardwert ist bereits im Anfangswert eines Feldes festgelegt, das zum Zeitpunkt der Vorbereitung festgelegt wurde (dies wird in JLS 5.4.2 beschrieben). Dieser Wert existiert also natürlich bereits zu Beginn des Lebenszyklus eines Feldes und ist daher eine logische Wahl für die Verwendung als Watchdog-Wert, der den Status dieses Felds überwacht. Mit diesen Regeln können Sie niemals den ursprünglichen Standardwert aus einem verzögerten statischen Feld abrufen. Hierzu kann die JVM beispielsweise ein Lazy Field als unveränderliche Verknüpfung zum entsprechenden Konstantenpoolelement implementieren.
Einschränkungen der Standardwerte können umgangen werden, indem die Werte (die möglicherweise den Standardwerten entsprechen) in Felder oder Container eines geeigneten Typs eingeschlossen werden. Eine Nullzahl kann in eine Ganzzahlreferenz ungleich Null eingeschlossen werden. Nicht-primitive Typen können in Optional eingeschlossen werden, das leer wird, wenn es auf null trifft.
Um die Freiheit bei der Implementierung von Features zu wahren, werden die Anforderungen an die isAssigned
Methode besonders unterschätzt. Wenn die JVM nachweisen kann, dass eine verzögerte statische Variable ohne beobachtbare externe Effekte initialisiert werden kann, kann sie diese Initialisierung jederzeit durchführen. In diesem Fall gibt isAssigned
true
auch wenn getfield
noch nie aufgerufen wurde. Die einzige Anforderung, die an isAssigned
wird, ist, dass bei der Rückgabe von false
keine der Nebenwirkungen der Variableninitialisierung im aktuellen Thread beobachtet werden sollten. Und wenn er true
zurückgibt, kann der aktuelle Thread in Zukunft die Nebenwirkungen der Initialisierung beobachten. Ein solcher Vertrag ermöglicht es dem Compiler, getstatic
für seine eigenen Felder durch ldc
zu ersetzen, wodurch die JVM keine detaillierten Zustände von getstatic
überwachen kann, die gemeinsame oder entartete Elemente im konstanten Pool haben.
Mehrere Threads können in einen Race-Status wechseln, um ein faules Endfeld zu initialisieren. Wie bereits bei CONSTANT_Dynamic
wählt die JVM einen beliebigen Gewinner dieses Rennens aus und stellt den Wert dieses Gewinners allen am Rennen teilnehmenden Threads zur Verfügung und schreibt ihn für alle nachfolgenden Versuche, einen Wert zu erhalten. Um das Rennen zu umgehen, versuchen bestimmte JVM-Implementierungen möglicherweise, CAS-Operationen zu verwenden. Wenn die Plattform diese unterstützt, wird dem Gewinner des Rennens der vorherige Standardwert und den Verlierern der nicht standardmäßige Wert angezeigt, der das Rennen gewonnen hat.
Somit funktionieren die bestehenden Regeln für die einmalige Zuweisung endgültiger Variablen weiterhin und erfassen nun alle Schwierigkeiten des Lazy Computing.
Die gleiche Logik gilt für das sichere Veröffentlichen mit endgültigen Feldern - sie gilt sowohl für verzögerte als auch für nicht verzögerte Felder.
Beachten Sie, dass eine Klasse ein statisches Feld in ein verzögertes statisches Feld konvertieren kann, ohne die Binärkompatibilität zu beeinträchtigen. Die getstatic
client getstatic
in beiden Fällen identisch. Wenn eine Variablendeklaration in Lazy getstatic
, wird getstatic
auf andere Weise verknüpft.
Alternative Lösungen
Sie können verschachtelte Klassen als Container für verzögerte Variablen verwenden.
Sie können so etwas wie eine Bibliotheks-API zum Verwalten von Lazy-Werten oder (allgemeiner) monotoner Daten definieren.
Refactor, was sie faulen statischen Variablen machen wollten, so dass sie zu statischen Nullmethoden wurden und ihre Körper in irgendeiner Weise unter Verwendung von ldc CONSTANT_Dynamic-Konstanten veröffentlicht wurden.
(Hinweis: Die obigen Problemumgehungen bieten keine binärkompatible Möglichkeit, vorhandene statische Konstanten evolutionär von ihrer <clinit>
zu entkoppeln.)
Wenn wir über die Bereitstellung weiterer Funktionen sprechen, können Sie zulassen, dass verzögerte Felder nicht statisch oder nicht endgültig sind, während die aktuellen Entsprechungen und Analogien zwischen dem Verhalten statischer und nicht statischer Felder beibehalten werden. Ein konstanter Pool kann kein Repository für nicht statische Felder sein, kann jedoch Bootstrap-Methoden enthalten (abhängig von der aktuellen Instanz). Gefrorene Arrays (falls implementiert) können eine verzögerte Option erhalten. Solche Studien sind eine gute Grundlage für zukünftige Projekte, die auf der Grundlage dieses Dokuments erstellt wurden. Übrigens machen solche Möglichkeiten unsere Entscheidung, Standardwerte zu verbieten, noch aussagekräftiger.
Lazy-Variablen müssen mit ihren eigenen Initialisierungsausdrücken initialisiert werden. Manchmal scheint dies eine sehr unangenehme Einschränkung zu sein, die uns in die Zeit der Erfindung leerer Endvariablen zurückversetzt. Denken Sie daran, dass diese leeren endgültigen Variablen mit beliebigen Codeblöcken einschließlich der Try-finally-Logik initialisiert werden können und nicht gleichzeitig, sondern in Gruppen initialisiert werden können. In Zukunft wird es möglich sein, die gleichen Möglichkeiten auf faule Endvariablen anzuwenden. Möglicherweise können eine oder mehrere verzögerte Variablen einem privaten Block von Initialisierungscode zugeordnet werden, dessen Aufgabe es ist, jede Variable genau einmal zuzuweisen, wie dies bei einem Klasseninitialisierer oder Objektkonstruktor der Fall ist. Die Architektur eines solchen Merkmals kann nach dem Auftreten von Dekonstruktoren klarer werden, da sich die Aufgaben, die sie lösen, in gewissem Sinne überschneiden.
Minute der Werbung. Die Joker 2018 Konferenz wird sehr bald stattfinden, wo es viele prominente Spezialisten für Java und JVM geben wird. Die vollständige Liste der Redner und Berichte finden Sie auf der offiziellen Website .
Der Autor
John Rose ist JVM-Ingenieur und Architekt bei Oracle. Leitender Ingenieur Da Vinci Machine Project (Teil von OpenJDK). Der leitende Ingenieur JSR 292 (Unterstützung dynamisch typisierter Sprachen auf der Java-Plattform) ist auf dynamische Aufrufe und verwandte Themen wie Typprofilerstellung und erweiterte Compileroptimierungen spezialisiert. Zuvor arbeitete er an inneren Klassen, erstellte den ursprünglichen HotSpot-Port für SPARC, Unsafe API, und entwickelte viele dynamische, parallele und hybride Sprachen, einschließlich Common Lisp, Scheme („esh“) und dynamischer Ordner für C ++.
Übersetzer
Oleg Chirukhin - Zum Zeitpunkt des Schreibens dieses Textes arbeitet er als Community Manager in der Firma JUG.ru Group und ist an der Popularisierung der Java-Plattform beteiligt. Bevor er zu JRG kam, war er an der Entwicklung von Bank- und Regierungsinformationssystemen, einem Ökosystem selbstgeschriebener Programmiersprachen und Online-Spielen beteiligt. Aktuelle Forschungsinteressen umfassen virtuelle Maschinen, Compiler und Programmiersprachen.