Il suffit qu'en Java, les loggers soient initialisés au moment de l'initialisation de la classe, pourquoi jonchent-ils tout le lancement? John Rose à la rescousse!
Voici à quoi cela pourrait ressembler:
lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
Ce document étend le comportement des variables finales, vous permettant éventuellement de prendre en charge l'exécution paresseuse - à la fois dans le langage lui-même et dans la JVM. Il est proposé d'améliorer le comportement des mécanismes existants de l'informatique paresseuse en modifiant la granularité: désormais, elle ne sera pas précise pour la classe, mais précise pour une variable spécifique.

La motivation
Java a profondément intégré l'informatique paresseuse. Presque chaque opération de liaison peut extraire du code paresseux. Par exemple, exécuter la méthode <clinit>
(bytecode de l'initialiseur de classe) ou utiliser la méthode bootstrap (pour un site d'appel dynamique invoqué ou des constantes CONSTANT_Dynamic
).
Les initialiseurs de classe sont quelque chose de très grossier en termes de granularité par rapport aux mécanismes utilisant des méthodes de bootstrap, car leur contrat est d'exécuter tout le code d'initialisation de la classe dans son ensemble , plutôt que de se limiter à l'initialisation liée à un champ spécifique de la classe. Les effets d'une telle initialisation brute sont difficiles à prévoir. Il est difficile d'isoler les effets secondaires de l'utilisation d' un champ statique d'une classe, car le calcul d'un champ conduit à calculer tous les champs statiques de cette classe.
Si vous touchez un champ, vous les affecterez tous. Dans les compilateurs AOT, cela rend particulièrement difficile l'optimisation des références de champ statiques, même pour les champs avec une valeur constante facilement analysable. Une fois qu'au moins un champ statique repensé est encombré parmi les champs, il devient impossible d'analyser complètement tous les champs de cette classe. Un problème similaire se manifeste avec les mécanismes précédemment proposés pour implémenter la convolution de constantes (pendant le fonctionnement en javac ) pour des champs constants avec des initialiseurs complexes.
Un exemple d'initialisation de champ repensée, qui se produit dans différents projets à chaque étape, dans chaque fichier, est l'initialisation de l'enregistreur.
private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
Cette initialisation inoffensive lance sous le capot une énorme quantité de travail qui sera effectuée pendant l'initialisation de la classe - et pourtant, il est extrêmement peu probable que l'enregistreur soit vraiment nécessaire au moment où la classe est initialisée, ou peut-être pas du tout nécessaire. La possibilité de différer sa création jusqu'à la première utilisation réelle simplifiera l'initialisation et, dans certains cas, cela aidera à éviter complètement cette initialisation.
Les variables finales sont très utiles, elles sont le principal mécanisme de l'API Java pour indiquer la constance des valeurs. Les variables paresseuses ont également bien fonctionné. À partir de Java 7, ils ont commencé à jouer un rôle de plus en plus important dans les @Stable
internes du JDK, marqués de l'annotation @Stable
. JIT peut optimiser les variables finales et stables - bien mieux que seulement certaines variables. L'ajout de variables finales paresseuses permettra à ce modèle d'utilisation utile de devenir plus courant, ce qui permettra de l'utiliser dans plus d'endroits. Enfin, l'utilisation de variables finales paresseuses permettra aux bibliothèques telles que le JDK de réduire la dépendance au code <clinit>
, ce qui devrait à son tour réduire le temps de démarrage et améliorer la qualité des optimisations AOT.
La description
Le champ peut être déclaré avec le nouveau modificateur lazy
, qui est un mot-clé contextuel perçu exclusivement comme un modificateur. Un tel champ est appelé champ paresseux et doit également avoir static
modificateurs static
et final
.
Un champ paresseux doit avoir un initialiseur. Le compilateur et le runtime conviennent de démarrer l'initialiseur exactement lorsque la variable est utilisée pour la première fois, et non lors de l'initialisation de la classe à laquelle ce champ appartient.
Chaque lazy static final
est associé au moment de la compilation à un élément de pool constant qui représente sa valeur. Puisque les éléments du pool constant eux-mêmes sont calculés paresseusement, il suffit d'attribuer simplement la bonne valeur pour chaque variable finale statique paresseuse associée à cet élément. (Vous pouvez lier plus d'une variable paresseuse à un élément, mais ce n'est guère une fonctionnalité utile ou significative.) Le nom d'attribut est LazyValue
, et il doit faire référence à un élément de genre constant qui peut être codé en ldc en une valeur qui peut être convertie en un type de champ paresseux. . Seuls les MethodHandle.invoke
déjà utilisés dans MethodHandle.invoke
.
Ainsi, un champ statique paresseux peut être considéré comme un alias nommé pour un élément de pool constant dans la classe qui a déclaré ce champ. Des outils comme les compilateurs peuvent en quelque sorte essayer d'utiliser ce champ.
Un champ paresseux n'est jamais une variable constante (au sens de JLS 4.12.4) et est explicitement exclu de la participation aux expressions constantes (au sens de JLS 15.28). Par conséquent, il ne capture jamais l'attribut ConstantValue
, même si son initialiseur est une expression constante. Au lieu de cela, le champ paresseux capture un nouveau type d'attribut de fichier de classes appelé LazyValue
, que la JVM consulte lors de la liaison à ce champ particulier. Le format de ce nouvel attribut est similaire au précédent, car il pointe également vers un élément du pool constant, dans ce cas, celui qui est résolu en valeur de champ.
Lorsqu'un champ statique paresseux est lié, le processus normal d'exécution des initialiseurs de classe ne doit pas disparaître. Au lieu de cela, toute méthode déclarante de classe <clinit>
est initialisée conformément aux règles définies dans JVMS 5.5. En d'autres termes, le bytecode getstatic
pour un champ statique paresseux effectue la même liaison que pour tout champ statique. Après l'initialisation (ou lors de l'initialisation déjà commencée du thread actuel), la JVM résout les éléments du pool constant associés au champ et stocke les valeurs obtenues à partir du pool constant dans ce champ.
Étant donné que la finale statique paresseuse ne peut pas être vide, aucune valeur ne peut leur être attribuée, même dans les quelques contextes où cela fonctionne pour les variables finales vides.
Lors de la compilation, tous les champs statiques paresseux sont initialisés indépendamment des champs statiques non paresseux, quel que soit leur emplacement dans le code source. Par conséquent, les restrictions sur l'emplacement des champs statiques ne s'appliquent pas aux champs statiques paresseux. L'initialiseur de champ statique paresseux peut utiliser n'importe quel champ statique de la même classe, quel que soit l'ordre dans lequel ils apparaissent dans la source. L'initialiseur de n'importe quel champ non statique ou l'initialiseur de classe peut accéder au champ paresseux, quel que soit l'ordre dans la source où ils sont les uns par rapport aux autres. Habituellement, faire cela n'est pas l'idée la plus raisonnable, car toute la signification des valeurs paresseuses est perdue, mais elle peut éventuellement être utilisée d'une manière ou d'une autre dans des expressions conditionnelles ou sur le flux de contrôle. Par conséquent, les champs statiques paresseux peuvent être traités plus comme des champs d'une autre classe - en ce sens qu'ils peuvent être référencés dans n'importe quel ordre à partir de n'importe quelle partie de la classe dans laquelle ils sont déclarés.
Les champs paresseux peuvent être détectés à l'aide de l'API de réflexion à l'aide de deux nouvelles méthodes d'API dans java.lang.reflect.Field
. La nouvelle méthode isLazy
renvoie true
si et seulement si le champ a un modificateur lazy
. La nouvelle méthode isAssigned
renvoie false
si et seulement si le champ est paresseux et n'est toujours pas initialisé au moment où isAssigned
. (Il peut retourner vrai presque au prochain appel dans le même thread, selon la présence de races). Il n'y a aucun moyen de savoir si un champ est initialisé, autre que d'utiliser isAssigned
.
(L'appel isAssigned
n'est nécessaire que pour résoudre les problèmes rares liés à la résolution des dépendances circulaires. Peut-être pouvons-nous nous passer de cette méthode. Cependant, les personnes qui écrivent du code avec des variables paresseuses veulent parfois savoir si la valeur est définie sur une telle variable ou pas encore, de la même manière que les utilisateurs de mutex veulent parfois savoir si le mutex est verrouillé ou non, mais ils ne veulent pas vraiment le verrouiller)
Il existe une limitation inhabituelle sur les champs finaux paresseux: ils ne doivent jamais être initialisés à leurs valeurs par défaut. Autrement dit, le champ de référence paresseux ne doit pas être initialisé à null
et les types numériques ne doivent pas avoir de valeur null. Une valeur booléenne paresseuse peut être initialisée avec une seule valeur - true
, puisque false
est sa valeur par défaut. Si l'initialiseur d'un champ statique paresseux renvoie sa valeur par défaut, la liaison de ce champ échouera avec l'erreur correspondante.
Cette restriction est introduite pour cela. pour permettre aux implémentations JVM de réserver des valeurs par défaut en tant que valeur de surveillance interne qui marque l'état d'un champ non initialisé. La valeur par défaut est déjà définie dans la valeur initiale de n'importe quel champ, définie au moment de la préparation (cela est décrit dans JLS 5.4.2). Cette valeur existe donc naturellement déjà au début du cycle de vie de n'importe quel champ, et est donc un choix logique à utiliser comme valeur de surveillance qui surveille l'état de ce champ. En utilisant ces règles, vous ne pouvez jamais obtenir la valeur par défaut d'origine à partir d'un champ statique paresseux. Pour cela, la JVM peut, par exemple, implémenter un champ paresseux en tant que lien immuable vers l'élément de pool constant correspondant.
Les restrictions sur les valeurs par défaut peuvent être contournées en encapsulant les valeurs (qui sont éventuellement égales aux valeurs par défaut) dans des boîtes ou des conteneurs d'un type pratique. Un nombre zéro peut être encapsulé dans une référence entière non nulle. Les types non primitifs peuvent être encapsulés dans Facultatif, qui devient vide s'il atteint null.
Pour conserver la liberté d'implémenter les fonctionnalités, les exigences de la méthode isAssigned
spécialement sous-estimées. Si la JVM peut prouver qu'une variable statique paresseuse peut être initialisée sans effets externes observables, elle peut effectuer cette initialisation à tout moment. Dans ce cas, isAssigned
retournera true
même si getfield
n'a jamais été appelé. La seule exigence imposée à isAssigned
est que si elle retourne false
, aucun des effets secondaires de l'initialisation des variables ne doit être observé dans le thread actuel. Et s'il est retourné true
, le thread actuel peut à l'avenir observer les effets secondaires de l'initialisation. Un tel contrat permet au compilateur de remplacer ldc
par getstatic
pour ses propres champs, ce qui permet à la JVM de ne pas surveiller les états détaillés des variables finales qui ont des éléments communs ou dégénérés dans le pool constant.
Plusieurs threads peuvent entrer dans un état de course pour initialiser un champ final paresseux. Comme cela se produit déjà avec CONSTANT_Dynamic
, la JVM sélectionne un gagnant arbitraire de cette course et fournit la valeur de ce gagnant à tous les threads participant à la course, et l'écrit pour toutes les tentatives ultérieures pour obtenir une valeur. Pour contourner la course, des implémentations JVM spécifiques peuvent essayer d'utiliser les opérations CAS, si la plate-forme les prend en charge, le vainqueur de la course verra la valeur par défaut précédente et les perdants verront la valeur non par défaut qui a remporté la course.
Ainsi, les règles existantes pour l'attribution unique des variables finales continuent de fonctionner et capturent désormais toutes les difficultés de l'informatique paresseuse.
La même logique s'applique à la publication sécurisée à l'aide des champs finaux - il en est de même pour les champs paresseux et non paresseux.
Notez qu'une classe peut convertir un champ statique en champ statique paresseux sans rompre la compatibilité binaire. L' getstatic
client getstatic
identique dans les deux cas. Lorsqu'une déclaration de variable devient paresseuse, getstatic
lié de manière différente.
Solutions alternatives
Vous pouvez utiliser des classes imbriquées comme conteneurs pour les variables paresseuses.
Vous pouvez définir quelque chose comme une API de bibliothèque pour gérer les valeurs paresseuses ou (plus généralement) toutes les données monotones.
Refactorisez ce qu'ils allaient faire des variables statiques paresseuses afin qu'elles se transforment en méthodes statiques nulles et que leurs corps soient publiés en utilisant les constantes ldc CONSTANT_Dynamic, d'une manière ou d'une autre.
(Remarque: les solutions de contournement ci-dessus ne fournissent pas un moyen compatible binaire de découpler de manière évolutive les constantes statiques existantes de leur <clinit>
)
Si nous parlons de fournir plus de fonctionnalités, vous pouvez autoriser les champs paresseux à être non statiques ou non finaux, tout en conservant les correspondances et analogies actuelles entre le comportement des champs statiques et non statiques. Un pool constant ne peut pas être un référentiel pour des champs non statiques, mais il peut toujours contenir des méthodes d'amorçage (selon l'instance actuelle). Les tableaux gelés (s'ils sont mis en œuvre) peuvent obtenir une option paresseuse. Ces études constituent une bonne base pour de futurs projets construits sur la base de ce document. Et en passant, ces opportunités rendent notre décision d'interdire les valeurs par défaut encore plus significative.
Les variables paresseuses doivent être initialisées à l'aide de leurs propres expressions d'initialisation. Parfois, cela semble être une limitation très désagréable qui nous ramène à l'époque de l'invention des variables finales vides. Rappelons que ces variables finales vides peuvent être initialisées avec des blocs de code arbitraires, y compris la logique try-finally, et elles peuvent être initialisées en groupes plutôt qu'en même temps. À l'avenir, il sera possible d'essayer d'appliquer les mêmes possibilités aux variables finales paresseuses. Peut-être qu'une ou plusieurs variables paresseuses peuvent être associées à un bloc privé de code d'initialisation dont la tâche consiste à affecter chaque variable exactement une fois, comme cela se produit avec un initialiseur de classe ou un constructeur d'objet. L'architecture d'une telle fonctionnalité peut devenir plus claire après l'apparition des déconstructeurs, car les tâches qu'ils résolvent se recoupent dans un certain sens.
Minute de publicité. La conférence Joker 2018 se tiendra très prochainement, où il y aura de nombreux spécialistes éminents de Java et de JVM. Consultez la liste complète des orateurs et les rapports sur le site officiel .
L'auteur
John Rose est ingénieur JVM et architecte chez Oracle. Ingénieur en chef Da Vinci Machine Project (partie d'OpenJDK). L'ingénieur principal JSR 292 (Prise en charge des langages à typage dynamique sur la plate-forme Java) est spécialisé dans les appels dynamiques et les sujets connexes tels que le profilage de type et les optimisations avancées du compilateur. Auparavant, il a travaillé sur les classes internes, créé le port HotSpot d'origine sur SPARC, l'API Unsafe, et a également développé de nombreux langages dynamiques, parallèles et hybrides, y compris Common Lisp, Scheme («esh»), des classeurs dynamiques pour C ++.
Traductrice
Oleg Chirukhin - au moment de la rédaction de ce texte, il travaille en tant que community manager dans la société JUG.ru Group, il est engagé dans la vulgarisation de la plateforme Java. Avant de rejoindre JRG, il a participé au développement de systèmes d'information bancaires et gouvernementaux, d'un écosystème de langages de programmation auto-écrits et de jeux en ligne. Les intérêts de recherche actuels incluent les machines virtuelles, les compilateurs et les langages de programmation.