¡Deja de alimentar a los madereros! ¡Da más modificadores! Campos finales estáticos perezosos. Borrador de croquis de funciones

Es suficiente que en Java, los registradores se inicialicen en el momento en que se inicializa la clase, ¿por qué ensucian todo el lanzamiento? John Rose al rescate!


Así es como podría verse:


lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar"); 

Este documento amplía el comportamiento de las variables finales, permitiéndole, opcionalmente, admitir la ejecución diferida, tanto en el lenguaje como en la JVM. Se propone mejorar el comportamiento de los mecanismos existentes de la computación diferida cambiando la granularidad: ahora no será precisa para la clase, sino precisa para una variable específica.



Motivación


Java ha construido profundamente en computación perezosa Casi todas las operaciones de enlace pueden extraer código diferido. Por ejemplo, ejecutando el método <clinit> ( <clinit> del inicializador de clase) o utilizando el método bootstrap (para un sitio de llamada dinámico invocado o constantes dinámicas CONSTANT_Dynamic ).


Los inicializadores de clase son algo muy grosero en términos de granularidad en comparación con los mecanismos que utilizan métodos de arranque, ya que su contrato es ejecutar todo el código de inicialización para la clase en su conjunto , en lugar de limitarse a la inicialización relacionada con un campo específico de la clase. Los efectos de tal cruda inicialización son difíciles de predecir. Es difícil aislar los efectos secundarios del uso de un campo estático de una clase, ya que calcular un campo lleva a calcular todos los campos estáticos de esta clase.


Si toca un campo, los afectará a todos. En los compiladores AOT, esto hace que sea particularmente difícil optimizar las referencias de campo estático, incluso para campos con un valor constante fácilmente analizable. Una vez que al menos un campo estático rediseñado está desordenado entre los campos, se hace imposible analizar completamente todos los campos de esta clase. Un problema similar se manifiesta con los mecanismos propuestos anteriormente para implementar la convolución de constantes (durante la operación javac ) para campos constantes con inicializadores complejos.


Un ejemplo de una inicialización de campo rediseñada, que ocurre en diferentes proyectos en cada paso, en cada archivo, es la inicialización del registrador.


 private final static Logger LOGGER = Logger.getLogger("com.foo.Bar"); 

Esta inicialización de aspecto inocuo lanza bajo el capó una gran cantidad de trabajo que se realizará durante la inicialización de la clase, y, sin embargo, es extremadamente improbable que el registrador sea realmente necesario en el momento en que se inicializa la clase, o tal vez no sea necesario. La capacidad de posponer su creación hasta el primer uso real simplificará la inicialización y, en algunos casos, ayudará a evitar esta inicialización por completo.


Las variables finales son muy útiles, son el mecanismo principal de la API de Java para indicar la constancia de los valores. Las variables perezosas también funcionaron bien. Comenzando con Java 7, comenzaron a desempeñar un papel cada vez más importante en las @Stable internas del JDK, y se marcaron con la anotación @Stable . JIT puede optimizar las variables finales y estables, mucho mejor que solo algunas variables. Agregar variables finales perezosas permitirá que este patrón de uso útil se vuelva más común, lo que permitirá su uso en más lugares. Finalmente, el uso de variables finales perezosas permitirá que las bibliotecas como el JDK reduzcan la dependencia del código <clinit> , lo que a su vez debería reducir el tiempo de inicio y mejorar la calidad de las optimizaciones AOT.


Descripción


El campo se puede declarar con el nuevo modificador diferido, que es una palabra clave contextual que se percibe exclusivamente como un modificador. Tal campo se llama campo perezoso y también debe tener modificadores static y final .


Un campo perezoso debe tener un inicializador. El compilador y el tiempo de ejecución acuerdan iniciar el inicializador exactamente cuando la variable se usa por primera vez, y no cuando se inicializa la clase a la que pertenece este campo.


Cada campo lazy static final diferido se asocia en tiempo de compilación con un elemento de agrupación constante que representa su valor. Dado que los elementos de la agrupación constante se calculan perezosamente, es suficiente asignar simplemente el valor correcto para cada variable final perezosa estática asociada con este elemento. (Puede vincular más de una variable diferida a un elemento, pero esta no es una característica útil o significativa). El nombre del atributo es LazyValue , y debe referirse a un elemento de género constante que puede codificarse con ldc en un valor que sea convertible a un tipo de campo diferido. . Solo MethodHandle.invoke lanzamientos que ya se utilizan en MethodHandle.invoke .


Por lo tanto, un campo estático vago puede considerarse como un alias con nombre para un elemento de agrupación constante dentro de la clase que declaró este campo. Herramientas como los compiladores de alguna manera pueden intentar usar este campo.


Un campo diferido nunca es una variable constante (en el sentido de JLS 4.12.4) y se excluye explícitamente de participar en expresiones constantes (en el sentido de JLS 15.28). Por lo tanto, nunca captura el atributo ConstantValue , incluso si su inicializador es una expresión constante. En cambio, el campo diferido captura un nuevo tipo de atributo de archivo de clase llamado LazyValue , que la JVM consulta cuando se vincula a este campo en particular. El formato de este nuevo atributo es similar al anterior, ya que también apunta a un elemento del grupo constante, en este caso, el que se resuelve en el valor del campo.


Cuando se vincula un campo estático diferido, el proceso normal de ejecución de inicializadores de clase no debe desaparecer. En cambio, cualquier método de declaración de clase <clinit> se inicializa de acuerdo con las reglas definidas en JVMS 5.5. En otras palabras, el bytecode getstatic para un campo estático getstatic realiza la misma vinculación que para cualquier campo estático. Después de la inicialización (o durante la inicialización ya iniciada del subproceso actual), la JVM resuelve los elementos de agrupación constante asociados con el campo y almacena los valores obtenidos de la agrupación constante en este campo.


Dado que el final estático diferido no puede estar vacío, no se les puede asignar ningún valor, incluso en los pocos contextos donde esto funciona para variables finales vacías.


Durante la compilación, todos los campos estáticos diferidos se inicializan independientemente de los campos estáticos no diferidos, independientemente de su ubicación en el código fuente. Por lo tanto, las restricciones sobre la ubicación de los campos estáticos no se aplican a los campos estáticos diferidos. El inicializador de campo estático diferido puede usar cualquier campo estático de la misma clase, independientemente del orden en que aparecen en la fuente. El inicializador de cualquier campo no estático o el inicializador de clase puede acceder al campo diferido, independientemente del orden en la fuente en el que estén relacionados entre sí. Por lo general, hacer esto no es la idea más sensata, ya que se pierde todo el significado de los valores perezosos, pero posiblemente se pueda usar de alguna manera en expresiones condicionales o en el flujo de control. Por lo tanto, los campos estáticos diferidos pueden tratarse más como campos de otra clase, en el sentido de que pueden referenciarse en cualquier orden desde cualquier parte de la clase en la que se declaran.


Los campos perezosos se pueden detectar utilizando la API de reflexión utilizando dos nuevos métodos de API en java.lang.reflect.Field . El nuevo método isLazy devuelve true si y solo si el campo tiene un modificador diferido. El nuevo método isAssigned devuelve false si y solo si el campo es vago y aún no se ha inicializado en el momento en que isAssigned . (Puede volverse verdadero casi en la próxima llamada en el mismo hilo, dependiendo de la presencia de razas). No hay forma de averiguar si se inicializa un campo, aparte de usar isAssigned .


(La llamada isAssigned es necesaria para ayudar con problemas raros relacionados con la resolución de dependencias circulares. Quizás podamos hacerlo sin implementar este método. Sin embargo, las personas que escriben código con variables perezosas a veces quieren saber si el valor está configurado para tal variable o no todavía, de la misma manera que los usuarios de mutex a veces quieren averiguar a través de mutex si está bloqueado o no, pero realmente no quieren bloquearse)


Hay una limitación inusual en los campos finales diferidos: nunca deben inicializarse a sus valores predeterminados. Es decir, el campo de referencia diferido no debe inicializarse como null , y los tipos numéricos no deben tener un valor nulo. Un valor booleano diferido se puede inicializar con un solo valor: true , ya que false es su valor predeterminado. Si el inicializador de un campo estático diferido devuelve su valor predeterminado, la vinculación de este campo fallará con el error correspondiente.


Esta restricción se introduce para eso. para permitir que las implementaciones de JVM reserven los valores predeterminados como un valor de vigilancia interno que marca el estado de un campo no inicializado. El valor predeterminado ya está establecido en el valor inicial de cualquier campo, establecido en el momento de la preparación (esto se describe en JLS 5.4.2). Por lo tanto, este valor ya existe naturalmente al comienzo del ciclo de vida de cualquier campo y, por lo tanto, es una opción lógica para usar como valor de vigilancia que supervisa el estado de este campo. Con estas reglas, nunca puede obtener el valor predeterminado original de un campo estático diferido. Para esto, la JVM puede, por ejemplo, implementar un campo diferido como un enlace inmutable al elemento de agrupación constante correspondiente.


Las restricciones sobre los valores predeterminados se pueden eludir envolviendo los valores (que posiblemente sean iguales a los valores predeterminados) en cajas o contenedores de algún tipo conveniente. Un número cero se puede incluir en una referencia de entero distinto de cero. Los tipos no primitivos se pueden ajustar en Opcional, que se vacía si llega a nulo.


Para mantener la libertad en las formas de implementar características, los requisitos para el método isAssigned subestiman especialmente. Si la JVM puede probar que una variable estática perezosa puede inicializarse sin efectos externos observables, puede hacer esta inicialización en cualquier momento. En este caso, isAssigned devolverá true incluso si nunca se ha llamado a getfield . El único requisito impuesto a isAssigned es que si devuelve false , ninguno de los efectos secundarios de la inicialización de variables debe observarse en el hilo actual. Y si volvió true , entonces el hilo actual puede observar en el futuro los efectos secundarios de la inicialización. Dicho contrato permite que el compilador reemplace ldc con getstatic para sus propios campos, lo que le permite a la JVM no monitorear estados detallados de variables finales que tienen elementos comunes o degenerados en el grupo constante.


Varios hilos pueden entrar en un estado de carrera para inicializar un campo final perezoso. Como ya sucede con CONSTANT_Dynamic , la JVM selecciona un ganador arbitrario de esta carrera y proporciona el valor de este ganador a todos los hilos que participan en la carrera, y lo escribe para todos los intentos posteriores de obtener un valor. Para sortear la carrera, las implementaciones específicas de JVM pueden intentar utilizar las operaciones CAS, si la plataforma las admite, el ganador de la carrera verá el valor predeterminado anterior y los perdedores verán el valor no predeterminado que ganó la carrera.


Por lo tanto, las reglas existentes para la asignación única de variables finales continúan funcionando y ahora capturan todas las dificultades de la computación diferida.


La misma lógica se aplica a la publicación segura utilizando campos finales: es la misma para los campos diferidos y no diferidos.


Tenga en cuenta que una clase puede convertir un campo estático en un campo estático lento sin romper la compatibilidad binaria. La declaración del cliente getstatic idéntica en ambos casos. Cuando una declaración variable cambia a perezosa, getstatic vincula de una manera diferente.


Soluciones alternativas


Puede usar clases anidadas como contenedores para variables perezosas.


Puede definir algo como una API de biblioteca para administrar valores diferidos o (más generalmente) cualquier información monótona.


Refactorice lo que iban a hacer variables estáticas perezosas para que se convirtieran en métodos estáticos nulos y sus cuerpos se publicaran usando constantes ldc CONSTANT_Dynamics, de alguna manera.


(Nota: Las soluciones anteriores no proporcionan una forma binaria compatible para desacoplar evolutivamente las constantes estáticas existentes de su <clinit> )


Si hablamos de proporcionar más funcionalidad, puede permitir que los campos diferidos sean no estáticos o no finales, mientras se mantienen las correspondencias y analogías actuales entre el comportamiento de los campos estáticos y no estáticos. Un grupo constante no puede ser un repositorio para campos no estáticos, pero aún puede contener métodos de arranque (dependiendo de la instancia actual). Las matrices congeladas (si se implementan) pueden obtener una opción perezosa. Dichos estudios son una buena base para futuros proyectos construidos sobre la base de este documento. Y, por cierto, tales oportunidades hacen que nuestra decisión de prohibir los valores por defecto sea aún más significativa.


Las variables perezosas deben inicializarse usando sus propias expresiones de inicialización. A veces esto parece una limitación muy desagradable que nos remite al momento de la invención de las variables finales vacías. Recuerde que estas variables finales vacías se pueden inicializar con bloques de código arbitrarios, incluida la lógica try-finally, y se pueden inicializar en grupos en lugar de simultáneamente. En el futuro, será posible intentar aplicar las mismas posibilidades a las variables finales perezosas. Quizás una o más variables perezosas pueden asociarse con un bloque privado de código de inicialización cuya tarea es asignar cada variable exactamente una vez, como sucede con un inicializador de clase o un constructor de objetos. La arquitectura de tal característica puede volverse más clara después de la aparición de los deconstructores, ya que las tareas que resuelven se cruzan en cierto sentido.


Minuto de publicidad. La Conferencia Joker 2018 se llevará a cabo muy pronto, donde habrá muchos especialistas destacados en Java y JVM. Vea la lista completa de oradores e informes en el sitio web oficial .

El autor


John Rose es ingeniero y arquitecto de JVM en Oracle. Ingeniero principal Da Vinci Machine Project (parte de OpenJDK). El ingeniero principal JSR 292 (Soporta lenguajes de tipo dinámico en la plataforma Java), se especializa en llamadas dinámicas y temas relacionados, tales como perfiles de tipo y optimizaciones avanzadas de compiladores. Anteriormente, trabajó en clases internas, creó el puerto HotSpot original en SPARC, API insegura, y también desarrolló muchos lenguajes dinámicos, paralelos e híbridos, incluidos Common Lisp, Scheme ("esh"), carpetas dinámicas para C ++.


Traductor


Oleg Chirukhin : en el momento de escribir este texto, trabajaba como administrador comunitario en la empresa JUG.ru Group, se dedica a la popularización de la plataforma Java. Antes de unirse a JRG, participó en el desarrollo de sistemas de información bancaria y gubernamental, un ecosistema de lenguajes de programación autoescritos y juegos en línea. Los intereses de investigación actuales incluyen máquinas virtuales, compiladores y lenguajes de programación.

Source: https://habr.com/ru/post/es422629/


All Articles