É suficiente que, em Java, os loggers sejam inicializados no momento em que a classe é inicializada, por que eles jogam lixo durante todo o lançamento? John Rose para o resgate!
Aqui está o que pode parecer:
lazy private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
Este documento estende o comportamento das variáveis finais, permitindo que você suporte opcionalmente a execução lenta - tanto no próprio idioma quanto na JVM. Propõe-se melhorar o comportamento dos mecanismos existentes da computação lenta, alterando a granularidade: agora não será preciso classificar, mas sim uma variável específica.

Motivação
Java construiu profundamente a computação lenta. Quase todas as operações de link podem arrancar código lento. Por exemplo, executando o método <clinit>
(bytecode do inicializador de classe) ou usando o método de autoinicialização (para um site de chamada <clinit>
ou constantes CONSTANT_Dynamic
).
Os inicializadores de classe são algo muito rude em termos de granularidade quando comparados aos mecanismos que usam métodos de autoinicialização, já que o contrato deles é executar todo o código de inicialização da classe como um todo , em vez de se limitar à inicialização relacionada a um campo específico da classe. Os efeitos dessa inicialização bruta são difíceis de prever. É difícil isolar os efeitos colaterais do uso de um campo estático de uma classe, pois o cálculo de um campo leva ao cálculo de todos os campos estáticos dessa classe.
Se você tocar em um campo, afetará todos eles. Nos compiladores AOT, isso torna particularmente difícil otimizar as referências de campo estático, mesmo para campos com um valor constante facilmente analisado. Uma vez que pelo menos um campo estático reprojetado está confuso entre os campos, torna-se impossível analisar completamente todos os campos dessa classe. Um problema semelhante se manifesta com os mecanismos propostos anteriormente para implementar a convolução de constantes (durante a operação javac ) para campos constantes com inicializadores complexos.
Um exemplo de uma inicialização de campo redesenhada, que ocorre em diferentes projetos a cada etapa, em cada arquivo, é a inicialização do criador de logs.
private final static Logger LOGGER = Logger.getLogger("com.foo.Bar");
Essa inicialização de aparência inócua lança sob o capô uma enorme quantidade de trabalho que será executado durante a inicialização da classe - e, no entanto, é extremamente improvável que o criador de logs seja realmente necessário no momento em que a classe é inicializada, ou talvez nem seja necessário. A capacidade de adiar sua criação até o primeiro uso real simplificará a inicialização e, em alguns casos, ajudará a evitar completamente essa inicialização.
As variáveis finais são muito úteis, elas são o principal mecanismo da API Java para indicar a constância dos valores. Variáveis preguiçosas também funcionaram bem. A partir do Java 7, eles começaram a desempenhar um papel cada vez mais importante nas @Stable
internas do JDK, sendo marcados com a anotação @Stable
. O JIT pode otimizar variáveis finais e estáveis - muito melhor do que apenas algumas variáveis. A adição de variáveis finais preguiçosas permitirá que esse padrão de uso útil se torne mais comum, possibilitando o uso em mais locais. Por fim, o uso de variáveis finais preguiçosas permitirá que bibliotecas como o JDK reduzam a dependência do código <clinit>
, o que, por sua vez, deve reduzir o tempo de inicialização e melhorar a qualidade das otimizações de AOT.
Descrição do produto
O campo pode ser declarado com o novo modificador lazy
, que é uma palavra-chave contextual que é percebida exclusivamente como um modificador. Esse campo é chamado de campo lento e também deve ter modificadores static
e final
.
Um campo lento deve ter um inicializador. O compilador e o tempo de execução concordam em iniciar o inicializador exatamente quando a variável é usada pela primeira vez, e não ao inicializar a classe à qual esse campo pertence.
Cada campo lazy static final
é associado no tempo de compilação a um elemento de pool constante que representa seu valor. Como os próprios elementos do pool constante são calculados preguiçosamente, basta atribuir o valor correto para cada variável final preguiçosa estática associada a esse elemento. (Você pode vincular mais de uma variável lenta a um elemento, mas isso dificilmente é um recurso útil ou significativo.) O nome do atributo é LazyValue
e deve se referir a um elemento de gênero constante que pode ser codificado por ldc em um valor que seja conversível em um tipo de campo lento . Somente conversões que já são usadas no MethodHandle.invoke
.
Assim, um campo estático lento pode ser considerado um alias nomeado para um elemento de pool constante dentro da classe que declarou esse campo. Ferramentas como compiladores podem, de alguma forma, tentar usar esse campo.
Um campo lento nunca é uma variável constante (no sentido de JLS 4.12.4) e é explicitamente excluído de participar de expressões constantes (no sentido de JLS 15.28). Portanto, ele nunca captura o atributo ConstantValue
, mesmo que seu inicializador seja uma expressão constante. Em vez disso, o campo lento captura um novo tipo de atributo de LazyValue
chamado LazyValue
, que a JVM consulta ao vincular a esse campo específico. O formato desse novo atributo é semelhante ao anterior, pois também aponta para um elemento do pool constante, nesse caso, aquele que é resolvido com o valor do campo.
Quando um campo estático lento é vinculado, o processo normal de execução dos inicializadores de classe não deve desaparecer. Em vez disso, qualquer método de classe <clinit>
declarante é inicializado de acordo com as regras definidas no JVMS 5.5. Em outras palavras, o bytecode getstatic
para um campo estático lento executa o mesmo vínculo que para qualquer campo estático. Após a inicialização (ou durante a inicialização já iniciada do encadeamento atual), a JVM resolve os elementos do conjunto constante associados ao campo e armazena os valores obtidos do conjunto constante nesse campo.
Como o final estático preguiçoso não pode estar vazio, eles não podem receber nenhum valor - mesmo nos poucos contextos em que isso funciona para variáveis finais vazias.
Durante a compilação, todos os campos estáticos preguiçosos são inicializados independentemente dos campos estáticos não preguiçosos, independentemente de sua localização no código-fonte. Portanto, as restrições no local dos campos estáticos não se aplicam aos campos estáticos preguiçosos. O inicializador de campo estático lento pode usar qualquer campo estático da mesma classe, independentemente da ordem em que eles aparecem na origem. O inicializador de qualquer campo não estático ou o inicializador de classe pode acessar o campo lento, independentemente da ordem na origem em que são relativos um ao outro. Geralmente, fazer isso não é a idéia mais sensata, pois todo o significado dos valores preguiçosos é perdido, mas pode ser usado de alguma maneira em expressões condicionais ou no fluxo de controle. Portanto, campos estáticos preguiçosos podem ser tratados mais como campos de outra classe - no sentido de que podem ser referenciados em qualquer ordem de qualquer parte da classe em que são declarados.
Campos preguiçosos podem ser detectados usando a API de reflexão usando dois novos métodos de API em java.lang.reflect.Field
. O novo método isLazy
retorna true
se e somente se o campo tiver um modificador lazy
. O novo método isAssigned
retorna false
se, e somente se, o campo estiver lento e ainda não inicializado no momento em que isAssigned
. (Ele pode retornar verdadeiro quase na próxima chamada no mesmo encadeamento, dependendo da presença de corridas). Não há como descobrir se um campo foi inicializado, exceto usando isAssigned
.
(A chamada isAssigned
é necessária isAssigned
para ajudar com problemas raros relacionados à resolução de dependências circulares. Talvez não possamos implementar esse método. No entanto, as pessoas que escrevem código com variáveis preguiçosas às vezes querem saber se o valor está definido para uma variável ou não ainda, da mesma maneira que os usuários do mutex às vezes desejam descobrir a partir do mutex se ele está bloqueado ou não, mas eles realmente não querem bloquear)
Há uma limitação incomum nos campos finais preguiçosos: eles nunca devem ser inicializados com seus valores padrão. Ou seja, o campo de referência lenta não deve ser inicializado como null
e os tipos numéricos não devem ter um valor nulo. Um valor booleano lento pode ser inicializado com apenas um valor - true
, pois false
é o valor padrão. Se o inicializador de um campo estático lento retornar seu valor padrão, a vinculação desse campo falhará com o erro correspondente.
Essa restrição é introduzida para isso. para permitir que as implementações da JVM reservem valores padrão como um valor de watchdog interno que marca o estado de um campo não inicializado. O valor padrão já está definido no valor inicial de qualquer campo, definido no momento da preparação (isso é descrito no JLS 5.4.2). Portanto, esse valor já existe naturalmente no início do ciclo de vida de qualquer campo e, portanto, é uma opção lógica para uso como um valor de vigilância que monitora o estado desse campo. Usando essas regras, você nunca pode obter o valor padrão original de um campo estático lento. Para isso, a JVM pode, por exemplo, implementar um campo lento como um link imutável para o elemento do conjunto constante correspondente.
Restrições nos valores padrão podem ser contornadas envolvendo os valores (que são possivelmente iguais aos valores padrão) em caixas ou recipientes de algum tipo conveniente. Um número zero pode ser quebrado em uma referência inteira diferente de zero. Tipos não primitivos podem ser agrupados em Opcional, que fica vazio se atingir nulo.
Para manter a liberdade nas maneiras de implementar recursos, os requisitos para o método isAssigned
especialmente subestimados. Se a JVM puder provar que uma variável estática lenta pode ser inicializada sem efeitos externos observáveis, ela poderá fazer essa inicialização a qualquer momento. Nesse caso, isAssigned
retornará true
mesmo que getfield
nunca tenha sido chamado. O único requisito imposto ao isAssigned
é que, se ele retornar false
, nenhum dos efeitos colaterais da inicialização da variável deve ser observado no encadeamento atual. E se ele retornou true
, o encadeamento atual poderá no futuro observar os efeitos colaterais da inicialização. Esse contrato permite que o compilador substitua ldc
por getstatic
por seus próprios campos, o que permite que a JVM não monitore estados detalhados de variáveis finais que possuem elementos comuns ou degenerados no pool constante.
Vários threads podem entrar em um estado de corrida para inicializar um campo final lento. Como já acontece com CONSTANT_Dynamic
, a JVM seleciona um vencedor arbitrário desta corrida e fornece o valor desse vencedor a todos os segmentos que participam da corrida e o grava para todas as tentativas subsequentes de obter um valor. Para contornar a corrida, implementações específicas da JVM podem tentar usar operações CAS, se a plataforma as suportar, o vencedor da corrida verá o valor padrão anterior e os perdedores verão o valor não padrão que venceu a corrida.
Assim, as regras existentes para a atribuição única de variáveis finais continuam funcionando e agora capturam todas as dificuldades da computação lenta.
A mesma lógica se aplica à publicação segura usando campos finais - é a mesma para campos preguiçosos e não preguiçosos.
Observe que uma classe pode converter um campo estático em um campo estático lento sem quebrar a compatibilidade binária. A getstatic
cliente getstatic
idêntica nos dois casos. Quando uma declaração de variável muda para lento, o getstatic
vinculado de uma maneira diferente.
Soluções alternativas
Você pode usar classes aninhadas como contêineres para variáveis preguiçosas.
Você pode definir algo como uma API de biblioteca para gerenciar valores preguiçosos ou (de maneira mais geral) quaisquer dados monótonos.
Refatorar o que eles tornariam variáveis estáticas preguiçosas para que se transformassem em métodos estáticos nulos e seus corpos fossem publicados usando constantes ldc CONSTANT_Dynamic, de alguma forma.
(Nota: As soluções alternativas acima não fornecem uma maneira binária compatível de desacoplar evolutivamente as constantes estáticas existentes de sua <clinit>
)
Se falamos em fornecer mais funcionalidade, você pode permitir que campos preguiçosos sejam não estáticos ou não finais, mantendo as correspondências e analogias atuais entre o comportamento de campos estáticos e não estáticos. Um pool constante não pode ser um repositório para campos não estáticos, mas ainda pode conter métodos de autoinicialização (dependendo da instância atual). Matrizes congeladas (se implementadas) podem obter uma opção lenta. Tais estudos são uma boa base para futuros projetos construídos com base neste documento. E, a propósito, essas oportunidades tornam nossa decisão de proibir valores padrão ainda mais significativos.
Variáveis preguiçosas devem ser inicializadas usando suas próprias expressões de inicialização. Às vezes, isso parece uma limitação muito desagradável que nos leva de volta ao tempo da invenção de variáveis finais vazias. Lembre-se de que essas variáveis finais vazias podem ser inicializadas com blocos de código arbitrários, incluindo a lógica try-finally, e podem ser inicializadas em grupos e não simultaneamente. No futuro, será possível tentar aplicar as mesmas possibilidades a variáveis finais preguiçosas. Talvez uma ou mais variáveis preguiçosas possam ser associadas a um bloco particular de código de inicialização, cuja tarefa é atribuir cada variável exatamente uma vez, como acontece com um inicializador de classe ou construtor de objeto. A arquitetura desse recurso pode se tornar mais clara após o aparecimento dos desconstrutores, uma vez que as tarefas que eles resolvem se cruzam em algum sentido.
Minuto de publicidade. A Conferência Joker 2018 será realizada muito em breve, onde haverá muitos especialistas de destaque em Java e JVM. Veja a lista completa de palestrantes e relatórios no site oficial .
O autor
John Rose é engenheiro e arquiteto de JVM na Oracle. Engenheiro Líder Da Vinci Machine Project (parte do OpenJDK). O engenheiro líder JSR 292 (Suporte a linguagens dinamicamente tipadas na plataforma Java) é especializado em chamadas dinâmicas e tópicos relacionados, como criação de perfil de tipo e otimizações avançadas de compilador. Anteriormente, ele trabalhou em classes internas, criou a porta HotSpot original em SPARC, API insegura e também desenvolveu muitas linguagens dinâmicas, paralelas e híbridas, incluindo Common Lisp, Scheme ("esh"), ligantes dinâmicos para C ++.
Tradutor
Oleg Chirukhin - no momento em que escrevia este texto, ele trabalhava como gerente de comunidade no grupo JUG.ru da empresa, e estava envolvido na popularização da plataforma Java. Antes de ingressar na JRG, ele participou do desenvolvimento de sistemas de informações bancárias e governamentais, um ecossistema de linguagens de programação auto-escritas e jogos online. Os interesses atuais de pesquisa incluem máquinas virtuais, compiladores e linguagens de programação.