Muito se fala da linguagem Java, das suas qualidades e dos seus defeitos, porém, muitas coisas interessantes acontecem no seu ecossistema, e uma dessas coisas foi a chegada da instrução invokedynamic
.
O surgimento
Essa nova instrução surgiu a partir do projeto Da Vinci Machine, com o intuito de expandir a máquina virtual Java para dar melhor suporte a outras linguagens além do Java, principalmente as dinâmicas, como Groovy.
O recurso foi lançado no Java 7 e teve seu primeiro uso na própria linguagem Java em sua release 8, para a implementação dos Lambdas, que em seu projeto inicial, utilizava classes anônimas.
Sua utilização
Hoje, esse recurso é utilizado por várias linguagens que rodam na JVM, principalmente as dinâmicas, e foi bastante utilizado na Nashorn (Uma engine Javascript para a JVM). Na época do surgimento, muito se discutia sobre os ganhos de performance nas linguagens dinâmicas, porém ela se mostrou útil até mesmo para a própria linguagem Java.
Lambdas
A primeira utilização pela linguagem Java foi nos Lambdas, por meio do LambdaMetafactory, que, em tempo de execução, gera uma classe que implementa o método da interface funcional utilizada.
Isso permite tanto que a máquina virtual Java detecte Lambdas com facilidade e as otimize, como diminui bastante o tamanho da classe final produzida, e a complexidade de implementação e evolução da feature.
Concatencão de Strings
Já no Java 9, a JEP 280 definiu especificações para a utilização do invokedynamic
nas concatenações de Strings, com o objetivo de facilitar a detecção de concatenação e a otimização delas, sem ser necessário criar novas instruções capazes de fazer uma concatenação mais eficiente.
Para isso, além da utilização da instrução, uma nova classe foi adicionada, a StringConcatFactory, além de diferentes estratégias de concatenação.
Record
Um outro uso curioso do recurso é nas classes record
, especificada pela JEP 359. A implementação dos métodos toString
, equals
e hashCode
utilizam invokedynamic
com a classe ObjectMethods.
Os motivos ainda não são tão claros, alguns argumentam que o motivo seria para reduzir o tamanho do arquivo final, o que não se prova verdade, já que para cada campo numa classe Record, um argumento novo precisa ser passado ao método bootstrap, como é especificado na própria documentação.
No entanto, é interessante ver como a instrução se prova útil na implementação de novos recursos, mesmo que não envolvam código dinâmico.
Pattern Matching
A JEP 406 especifica melhorias de pattern matching para o switch-case, e esse recurso, pelo menos até o momento que escrevi o artigo, está utilizando o invokedynamic
junto a classe SwitchBootstraps. Com isso, nenhuma mudança precisa ser feita na JVM para suportar esse novo modelo de switch-case, já que a conversão é feita em tempo de execução.
Acredito eu que o principal motivo do uso é pelas instruções lookupswitch
e tableswitch
serem int-based, ou seja, a máquina virtual Java implementa o mecanismo de switch
apenas para números. Devido a isso, ao fazer um switch-case
para um Enum ou para uma String, o Enum é convertido para um número usando a propriedade ordinal
e as Strings passam por dois processos, o primeiro é a conversão para um número usando o hashCode
e o segundo passo é fazendo uma comparação com equals
, para evitar conflito de hash.
Inclusive, no caso dos Enums, para evitar problemas quando o Enum em questão é modificado, podendo resultar em um valor ordinal
diferente em tempo de execução, é feito um mapeamento em uma classe sintética anônima. Um Enum ter um valor ordinal
diferente em tempo de execução, do que era em compilação, é comum, já que ele pode pertencer a bibliotecas externas que podem ser modificadas recebendo novos Enums ou até perdendo alguns deles. Me lembro inclusive de sofrer um pouco com esse detalhe ao implementar o Kores, tanto nos Enums quanto nas Strings.
Sem entrar em muitos detalhes, no caso das mudanças que o Pattern Matching entrega, a classe SwitchBootstraps é utilizada para converter o valor de entrada para um número inteiro (integer) que é utilizado para decidir qual é a entrada case do switch que deve ser seguida.
No quesito de performance, o switch-case tem o melhor e pior caso em O(1) para TableSwitch, porém, pelo menos até o momento, o javac
tem gerado LookupSwitch para esses casos, que tem uma complexidade O(log n), o que não deve (e nem vai) ser um problema.
Futuro
Já há uma issue aberta e uma JEP (JEP 405) para melhorar o Pattern Matching e permitir destructuring para Arrays e Record Types, estou curioso para saber qual abordagem será utilizada, mas invokedynamic
continua fazendo sentido e tem se mostrado extremamente útil.
Qual tem sido o papel da instrução
A instrução invokedynamic
tem tido um papel importante no avanço da linguagem Java, mesmo que seu propósito inicial tenha sido outro.
O primeiro ponto importante é que, ela fornece uma estrutura muito fácil de ser detectada e otimizada, já que ela permite que lógicas complexas sejam abstraídas atrás de uma única instrução. Por exemplo, a concatenação de Strings normalmente envolve criar um StringBuilder e fazer diversas chamadas de append, resultando em várias instruções utilizadas na classe final, com o invokedynamic
, uma única instrução especifica todos valores a serem trabalhados. A maquina virtual, ao ver essa instrução, sabe exatamente o que ela abstrai, e pode, a partir dai, utilizar uma implementação otimizada ou fazer inlining.
O segundo ponto é que ela tira a necessidade de modificar a máquina virtual Java para suportar novos recursos, como é o caso da JEP 406, que pode ser implementada totalmente sem a necessidade de mudanças na máquina virtual. Mesmo que algumas coisas ainda pudessem ser implementadas apenas modificando o compilador, elas adicionariam mais complexidade ao mesmo, e melhorias de performance só seriam notadas ao recompilar o código, o que vai contra o propósito da linguagem Java.
Um outro ponto que eu acho importante ressaltar, é que ela abre portas para a implementação de muitos outros recursos — que antes envolveriam modificar a máquina virtual ou modificar o compilador, ou os dois juntos — sem ter um impacto grande na performance e ainda sendo facilmente detectáveis e otimizáveis. Incluindo o fato de que outras linguagens, como Scala, Groovy e Kotlin, podem tirar vantagens dela.
Além disso, ela motiva a implementação de vários recursos utilizando a própria linguagem Java, ao invés de aumentar a complexidade dos códigos na linguagem C/C++, que são utilizadas na implementação da JVM. Isso segue um pouco no propósito da própria GraalVM e do projeto Metropolis, não exatamente o que esses projetos tem como objetivo, mas se aproxima na ideia de ter cada vez mais recursos implementados na própria linguagem Java do que em C/C++.
Evoluções
Constantes dinâmicas
Uma das evoluções mais recentes foi a entregue pelo JEP 309, onde constantes agora podem ser calculadas dinamicamente, aumentando a flexibilidade da JVM no quesito de constantes, que hoje se limitam a alguns tipos de valores, como primitivos, strings, classes, etc, mas nunca a tipos definidos pelo usuário.
Com essa entrega, constantes agora podem ser calculadas por um método de bootstrap
e armazenadas pelo resto da execução da aplicação, em forma de contantes.
Exposição da API
Uma outra ideia extremamente interessante é a JEP 303 que tem a finalidade de expor tanto a instrução invokedynamic
quanto a instrução ldc
, por meio de uma API Java, na qual o compilador iria transformar diretamente para as instruções correspondentes (fazendo algo parecido com o que ele já faz com MethodHandle
e @PolymorphicSignature
).
Essa exposição seria extremamente útil para tirar vantagem desse recurso sem a necessidade de criar o próprio compilador, fazer transformações, ou qualquer abordagem que é necessária caso queiramos utilizar essa instrução.
Com isso, poderiamos criar constantes de tipos customizados e fazer invocações dinâmicas, direto da linguagem Java.
Como funciona
Falamos e falamos bem do invokedynamic
, mas como ela funciona?
Bom, essa instrução funciona com base em um conceito bem simples até, onde você tem um método bootstrap
que é responsável pela resolução do método destino a ser invocado, e, após resolvido, as invocações subsequentes vão diretamente para esse método, sem passar pelo bootstrap
novamente.
Toda a interface com a instrução é feita por meio da API java.lang.invoke
que foi introduzida na versão 7. Com essa API, temos acesso a invocação de métodos e acesso a campos em um nível mais baixo que a API de Reflection, inclusive, essa API é extremamente rápida, já que ela oferece uma interface que entrega uma performance muito próxima a uma invocação gerada pelo compilador. Em troca, você precisa saber o tipo de retorno e os tipos de parâmetros exatos do método que quer invocar.
Linking
Primeiramente, o call site de um invokedynamic
se inicia em um estado não-ligado, o que quer dizer que não está ligado com nenhum alvo de invocação. Antes que a invocação possa acontecer, a instrução precisa primeiramente ser linkada, nesse momento, o método bootstrap
é chamado e ele irá retornar um CallSite, que dita o comportamento da invocação.
Depois de linkado, todas invocações subsequentes serão direcionadas ao target do CallSite, esse, que por sua vez, pode ser imutável, do tipo ConstantCallSite ou mutável do tipo MutableCallSite ou VolatileCallSite. Todas essas classes podem ser estendidas para fornecer sua própria lógica de CallSite, e você pode retornar esse tipo no seu método bootstrap
que ele ficará linkado com a instrução.
Uma coisa importante de se saber é que, ligar uma instrução invokedynamic
a um CallSite ocorre no máximo uma vez, do estado unlinked para linked, mas provavelmente nunca de linked para unlinked (isso depende da implementação da JVM, mas acredito que atualmente todas seguem esse modelo especificado nos documentos da Oracle). O que isso quer dizer? Isso quer dizer que após ocorrer a ligação entre o CallSite e a instrução, o método bootstrap nunca mais será chamado para aquele CallSite, permanecendo ligado pelo resto da execução.
Apesar disso, você ainda pode mudar o target de um CallSite mutável, permitindo que outro método seja escolhido para ser invocado. No entanto, você não consegue deslinkar a instrução de um CallSite e faze-lo reexecutar o bootstrap. Isso é um dos detalhes de implementação descritos na documentação.
Constantes
Com a implementação da JEP 309, podemos “criar” constantes dinâmicas, algo que antes nunca foi possível.
Qual a diferença?
As constantes são valores armazenados na região chamada de Constant Pool, numa classe Java compilada. Diferente dos campos, os valores constantes tem garantia de imutabilidade, ou seja, nunca mudam. São ótimos alvos de otimização e sua existência é extremamente importante para o mecanismo invokedynamic
.
Mas por qual motivo?
Os métodos bootstrap
recebem argumentos que sempre são constantes que residem na Constant Pool, inclusive, na implementação do invokedynamic
, foi necessário introduzir novos tipos de constantes, como MethodHandle e MethodType.
Com a introdução de constantes dinâmicas, ao invés de criar novos tipos de constantes, podemos criar constantes que são computadas por meio de métodos bootstrap
, que podem ser de qualquer tipo, e carregadas usando ldc
ou utilizadas como argumentos para os métodos bootstrap
.
E como funciona?
Não muito diferente de como funciona a instrução invokedynamic
. Porém, ao invés do método de bootstrap
receber um MethodType, como terceiro parâmetro, ele recebe uma Class<?>
, e ao invés de retornar um CallSite, ele retorna o valor da constante que deve ser do mesmo tipo da Class<?>
recebida.
Tanto as constantes como as invocações dinâmicas, se comportam de uma maneira muito próxima a um lazy
, onde o bootstrap
é chamado na primeira vez, e depois o valor é salvo e reutilizado nas demais chamadas.
Uso prático e mais documentos
Infelizmente, invokedynamic
ainda é algo muito especifico, apesar de poderoso, seu uso mais frequente nas features do Java mostram que essa instrução ajuda muito na introdução rápida de novos recursos sem a necessidade de aumentar o escopo do projeto e sem precisar de mudanças na JVM.
Apesar de ser ótima para linguagens dinâmicas, também é importante ressaltar que ela não resolve todos os desafios que linguagens dinâmicas sofrem, e as vezes, elas precisam fazer um fallback para Reflection ou um modelo de resolução alternativo, pois a instrução não atende aos requisitos de performance, como em alguns casos, na linguagem Groovy.
InvokeDynamic é ótimo em diversos cenários, porém existem situações aonde a JVM ainda tem dificuldade em otimizar as invocações, quando comparado as suas capacidades bem maturas de otimizar Reflection.
E ainda é um recurso um pouco distante das mãos dos programadores, sendo muito focado para os compiladores e linguagens de programação, porém bibliotecas como o ByteBuddy pode ajudar você a colocar as mãos nesse recurso, caso encontre cenários onde isso seria perfeito, como implementação de Duck-Typing.
Para mais detalhes você pode olhar a documentação do Kores, ou esperar um tempinho até que o Guide esteja pronto.
Conclusão
invokedynamic
tem tido um grande papel na entrega não só de melhorias nas linguagens dinâmicas, como também na evolução da linguagem Java. Esse recurso permite a entrega de novas features sem a necessidade de modificar ou implementar novos recursos na máquina virtual Java, algo extremamente importante nesse novo modelo de lançamento de versões com uma maior frequência.
Nesse artigo entendemos como ela funciona, e como a linguagem Java tem tirado proveito de um recurso antes focado em outras linguagens, e como essas mudanças podem também ajudar no futuro da linguagem.