Como aquecer a JVM
*1. Visão geral *
A JVM é uma das mais antigas e poderosas máquinas virtuais já construídas.
Neste artigo, veremos rapidamente o que significa aquecer uma JVM e como fazê-lo.
===* 2. Noções básicas da arquitetura da JVM *
Sempre que um novo processo da JVM é iniciado, todas as classes necessárias são carregadas na memória por uma instância do ClassLoader. Esse processo ocorre em três etapas:
-
Carregamento da classe do aplicativo : o AppClassLoader carrega todas as classes localizadas no caminho da classe do aplicativo.
Esse processo de inicialização é baseado em um esquema de carregamento lento.
*3. O que está aquecendo a JVM *
Depois que o carregamento da classe é concluído, todas as classes importantes (usadas no momento do início do processo) são enviadas para https://www.ibm.com/support/knowledgecenter/en/SSAW57_8.5.5/com.ibm.websphere.nd .doc/ae/rdyn_tunediskcache.html [Cache da JVM (código nativo)] - que os torna acessíveis mais rapidamente durante o tempo de execução. Outras classes são carregadas por solicitação.
A primeira solicitação feita a um aplicativo da Web Java geralmente é substancialmente mais lenta que o tempo médio de resposta durante a vida útil do processo. Esse período de aquecimento geralmente pode ser atribuído ao carregamento lento da classe e à compilação just-in-time.
Tendo isso em mente, para aplicativos de baixa latência, precisamos armazenar em cache todas as classes antecipadamente - para que elas estejam disponíveis instantaneamente quando acessadas em tempo de execução.
*Esse processo de ajuste da JVM é conhecido como aquecimento.*
*4. Compilação em camadas *
Graças à arquitetura de som da JVM, os métodos usados com frequência são carregados no cache nativo durante o ciclo de vida do aplicativo.
Podemos usar essa propriedade para forçar o carregamento de métodos críticos no cache quando um aplicativo é iniciado. Nessa medida, precisamos definir um argumento da VM chamado* _Tiered Compilation _: *
-XX:CompileThreshold -XX:TieredCompilation
Normalmente, a VM usa o intérprete para coletar informações de criação de perfil sobre métodos que são alimentados no compilador. No esquema em camadas, além do intérprete, o compilador do cliente é usado para gerar versões compiladas de métodos que coletam informações de criação de perfil sobre si mesmos.
Como o código compilado é substancialmente mais rápido que o código interpretado, o programa é executado com melhor desempenho durante a fase de criação de perfil.
Os aplicativos em execução no JBoss e JDK versão 7 com esse argumento da VM ativado tendem a falhar após algum tempo devido a um bug documentado. O problema foi corrigido no JDK versão 8.
Outro ponto a ser observado aqui é que, para forçar o carregamento, precisamos garantir que todas (ou a maioria) das classes que serão executadas precisam ser acessadas. É semelhante a determinar a cobertura do código durante o teste de unidade. Quanto mais código for coberto, melhor será o desempenho.
A próxima seção demonstra como isso pode ser implementado.
===* 5. Implementação manual *
Podemos implementar uma técnica alternativa para aquecer a JVM. Nesse caso, um simples aquecimento manual pode incluir a repetição da criação de diferentes classes milhares de vezes, assim que o aplicativo é iniciado.
Primeiro, precisamos criar uma classe fictícia com um método normal:
public class Dummy {
public void m() {
}
}
Em seguida, precisamos criar uma classe que tenha um método estático que será executado pelo menos 100000 vezes assim que o aplicativo iniciar e, a cada execução, cria uma nova instância da classe dummy mencionada anteriormente, que criamos anteriormente:
public class ManualClassLoader {
protected static void load() {
for (int i = 0; i < 100000; i++) {
Dummy dummy = new Dummy();
dummy.m();
}
}
}
Agora, para* medir o ganho de desempenho *, precisamos criar uma classe principal. Esta classe contém um bloco estático que contém uma chamada direta ao método load () _ _ManualClassLoader.
Dentro da função principal, fazemos uma chamada para o método load () _ManualClassLoader mais uma vez e capturamos o tempo do sistema em nanossegundos antes e depois da nossa chamada de função. Finalmente, subtraímos esses tempos para obter o tempo real de execução.
Temos que executar o aplicativo duas vezes; uma vez com a chamada do método _load () _ dentro do bloco estático e uma vez sem essa chamada do método:
public class MainApplication {
static {
long start = System.nanoTime();
ManualClassLoader.load();
long end = System.nanoTime();
System.out.println("Warm Up time : " + (end - start));
}
public static void main(String[] args) {
long start = System.nanoTime();
ManualClassLoader.load();
long end = System.nanoTime();
System.out.println("Total time taken : " + (end - start));
}
}
Abaixo, os resultados são reproduzidos em nanossegundos:
With Warm Up |
No Warm Up |
*Difference(%) * |
1220056 |
8903640 |
730 |
1083797 |
13609530 |
1256 |
1026025 |
9283837 |
905 |
1024047 |
7234871 |
706 |
868782 |
9146180 |
1053 |
Como esperado, a abordagem de aquecimento mostra um desempenho muito melhor que o normal.
Obviamente, esse é um* ponto de referência muito simplista *e fornece apenas algumas informações no nível da superfície sobre o impacto dessa técnica. Além disso, é importante entender que, com um aplicativo do mundo real, precisamos nos aquecer com os caminhos de código típicos no sistema.
===* 6. Ferramentas *
Também podemos usar várias ferramentas para aquecer a JVM. Uma das ferramentas mais conhecidas é o Java Microbenchmark Harness, JMH. Geralmente é usado para micro-benchmarking. Depois de carregado,* ele atinge repetidamente um trecho de código e monitora o ciclo de iteração de aquecimento. *
Para usá-lo, precisamos adicionar outra dependência ao pom.xml:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
</dependency>
Podemos verificar a versão mais recente do JMH em Repositório Central do Maven.
Como alternativa, podemos usar o plugin maven do JMH para gerar um projeto de amostra:
mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=com. \
-DartifactId=test \
-Dversion=1.0
Em seguida, vamos criar um método main:
public static void main(String[] args)
throws RunnerException, IOException {
Main.main(args);
}
Agora, precisamos criar um método e anotá-lo com a anotação _ @ Benchmark_ da JMH:
@Benchmark
public void init() {
//code snippet
}
Dentro deste método init, precisamos escrever um código que precise ser executado repetidamente para aquecer.
===* 7. Referência de desempenho *
Nos últimos 20 anos, a maioria das contribuições para Java estava relacionada ao GC (Garbage Collector) e JIT (Just In Time Compiler). Quase todos os benchmarks de desempenho encontrados online são feitos em uma JVM já em execução há algum tempo. Contudo,
No entanto, a Beihang University publicou um relatório de benchmark levando em consideração o tempo de aquecimento da JVM. Eles usaram sistemas baseados no Hadoop e Spark para processar dados massivos:
link:/wp-content/uploads/2017/06/jvm.png [imagem:/wp-content/uploads/2017/06/jvm-300x222.png [imagem]]
Aqui, o HotTub designa o ambiente em que a JVM foi aquecida.
Como você pode ver, a velocidade pode ser significativa, especialmente para operações de leitura relativamente pequenas - e é por isso que esses dados são interessantes a considerar.
===* 8. Conclusão*
Neste artigo rápido, mostramos como a JVM carrega classes quando um aplicativo é iniciado e como podemos aquecer a JVM para obter um aumento no desempenho.
Este livro repassa mais informações e diretrizes sobre o tópico, se você quiser continuar.
E, como sempre, o código fonte completo está disponível over no GitHub.