HotSpot e JIT Compiler, entendendo como funciona!

Vimos no post anterior como a JVM funciona e como ela detecta e analisa os pontos “quentes” da aplicação. Vimos que os mecanismos que fazem essa análise e melhoria são: HotSpot e JIT(just-in-time compiler) Compiler. Neste post, iremos entender como eles funcionam.

Introdução

Antes de falarmos mais profundamente sobre cada técnica, tentaremos dar um resumo rápido sobre cada uma delas, como segue abaixo:

HotSpot
Quando se executa a aplicação na primeira vez, ela é executada em modo interpretado, ou seja, não é feita nenhuma otimização inicial. Durante a utilização da aplicação, são feitas estatísticas de runtime(durante a execução). Com essas estatísticas é verificado os pontos quentes, os trechos de códigos mais executados do programa. Esses trechos são otimizados, utilizando técnicas bem parecidas com as dos compiladores tradicionais, mas, lembrando que isso tudo é feito em runtime. O HotSpot utiliza a idéia de que apenas 10% do código é executado em 90% do tempo, então a otimização destes pontos isolados permitem um ganho de performance e em até algumas vezes maior do que a compilação tradicional, pois os dados são obtidos diretos da execução real do programa.

JIT Compiler
Compiladores tradicionais(nativos), as otimizações são feitas em tempo de compilação, reordenando instruções, colocando métodos inline(que será explicado mais a frente), otimizando para a máquina que está compilando etc. Isso claro para linguagens como C++ etc.

No Java, isso não funciona: pelo simples fato de que as classes são compiladas em arquivos separados. O compilador JIT irá pegar o bytecode e traduzir tudo de uma vez para a linguagem de máquina antes da execução, esta é a estratégia utilizada pela maioria das VMs. Isso pode ser até um pouco lento inicialmente, mas veremos mais a frente que isso não será um problema.

Aprendendo mais…

O Java por não compilar para código nativo(código da máquina), ou seja, ele não tem um bytecode/executável “preso” a máquina onde ele foi compilado, ele pode e vai usar estratégias focadas no sistema operacional e no hardware usados no momento de execução, podendo até em alguns momentos invocar instruções específicas do processador da máquina.

Em alguns momentos a VM pode perceber que um determinado código já não é mais importante e descartar suas compilações ou até perceber que a otimização que ele fez na primeira vez não foi tão boa assim e pode ser melhorada, dessa forma, ele vai novamente e aplica um novo tipo de otimização e esse ciclo pode ocorrer várias e várias vezes, ou seja, ele pode ficar sempre tentando otimizar seu código ao máximo.

Dessa forma, o seu código irá ficar cada vez mais rápido, devido principalmente a evolução constante da JVM e do JIT. Mesmo programas mais antigos podem ter sua performance melhorada simplesmente atualizando a JVM, sem precisar modificar qualquer linha de código.

Isso já não acontece com um programa nativo(Ex: Word, Excel), onde teríamos que esperar do fabricante uma versão nova recompilada, tanto para o SO atual como para o hardware, processador etc. Quando o fabricante faz uma melhoria no programa, Word por exemplo, você terá que baixar uma atualização dele, um novo “executável”, isso porque os programadores refizeram o código, adicionaram suas melhorias e compilarão novamente, gerando assim o executável.

Essa otimização funciona também para linguagens que rodam em cima da JVM, como o Ruby e várias outras.

Técnica de Inline
Inline de métodos é uma das técnicas utilizada pelo JIT para evitar fazer mais de uma chamada do método.

Veremos um exemplo bem comum:

public class TesteLoop {

	public static void main(String[] args) {
		for (int i = 0; i < new ArrayList().size(); i++) {
			metodoUM();
		}
		metodoDOIS();
	}

	public static void metodoUM() {
		System.out.println("bloco de codigo do metodo UM");
		metodoDOIS();
	}

	public static void metodoDOIS() {
		System.out.println("bloco de codigo do metodo DOIS");
	}

}

No métodomain , existe um loop, onde o mesmo faz referencia ao metodoUM() , ele será executado neste loop 20 vezes. Neste momento o HotSpot irá verificar que esse método main é um código quente, e o JIT irá fazer o inline do método:

	public static void main(String[] args) {
		for (int i = 0; i < new ArrayList().size(); i++) {
			System.out.println("bloco de codigo do metodo UM");
		}
		metodoDOIS();
	}

Já ométodoUM() , possui também uma chamada para um outro método, o metodoDOIS() …

	public static void metodoUM() {
		System.out.println("bloco de codigo do metodo UM");
		System.out.println("bloco de codigo do metodo DOIS");
	}

	public static void metodoDOIS() {
		System.out.println("bloco de codigo do metodo DOIS");
	}

No final, com o inline “aplicado” a nossa classe ficaria algo semelhante a isso:

public class TesteLoop {

	public static void main(String[] args) {
		for (int i = 0; i < new ArrayList().size(); i++) {
			System.out.println("bloco de codigo do metodo UM");
			System.out.println("bloco de codigo do metodo DOIS");
		}
		System.out.println("bloco de codigo do metodo DOIS");
	}
}

O HotSopt também não irá verificar somente a quantidade de vezes que o método é executado, mas também o empilhamento de métodos e chamadas de um método para outro(jumps). Omain  chama ometodoUM() , que ele chama ometodoDOIS() , há um empilhamento de métodos. Outro exemplo neste caso:

Exemplo:

public class Teste {  

    public static void main(String[] args) {  
        outroMetodo();  
    }  

    public static final void outoMetodo() {  
        System.out.println("Funcionalidade do método: OutroMetodo");  
    }  
}

Inline:

public class Teste {

	public static void main(String[] args) {
		System.out.println("Funcionalidade do método: inline");
	}
}

O HotSpot detecta que esse método está sendo chamado várias vezes, seja por um loop ou por outros métodos, quando percebe que o método está sendo executado várias vezes, então ele aciona o JIT, que “extrai” o conteúdo do método (outroMetodo ) e adiciona diretamente no método principal(teste ). Essa quantidade de vezes pode e vai variar dependendo do tipo de perfil utilizado pela JVM, claro que o exemplo aqui foi apenas para ilustrar, mas para você ter uma ideia a JVM por padrão aguarda 1.500 invocações ao método até ele ser compilado, já no perfil server, ele irá aguardar em torno de 10.000 invocações.

Tipos de Perfis

Quando desenvolvemos sistemas, sabemos que cada aplicação tem as suas necessidades, sejam de performance, escalabilidade e responsividade. A JVM oferece alguns perfis que adaptam o “motor” JIT e outras características dela para atingir esses objetivos.

Temos então basicamente dois tipos de perfis: Cliente (client) e Servidores (server).

Perfil Cliente

Definindo o perfil “client” algumas configurações e comportamentos serão tomados para com a aplicação. Esse perfil é utilizado para aplicações voltadas para o usuário final, como aplicações desktop, onde a mesma tem que ser inicializada rapidamente, embora depois não precise de tanta performance.

Neste perfil precisamos que a JVM perca menos tempo no startup da aplicação, para que o usuário veja o resultado mais rapidamente, em outras palavras, precisamos que a aplicação Java “abra” mais rápido. Mas isso tem um custo, que é ter um JIT que irá fazer menos otimizações a longo prazo. O foco deste perfil é de consumir menos memória, então, para isso ela deixa de fazer certas otimizações no JIT para evitar o uso excessivo de memória, deixando até de fazer as vezes  o inline de métodos.

Perfil Servidor

Já neste perfil, não temos a preocupação que a aplicação seja iniciada rapidamente, como um sistema que está rodando em um cliente, onde ele pode esperar até que o sistema inicialize, ou um sistema “intranet”, ou até um site ou mesmo um fórum, mas temos uma grande preocupação em performance a longo prazo.

Ele irá demorar mais para startar/inicializar, por outro lado o JIT irá rodar no seu modo mais “completo”, com suas melhores estratégias de otimização para melhorar o desempenho da sua aplicação. Ao contrário do perfil de Cliente, o uso de memória deste tipo de perfil será maior e utilizado com mais frequência,  por outro lado, iremos ganhar uma performance maior e a longo prazo, como o uso de inlines para evitar jumps(chamadas de métodos encadeados, como explicado no inline mais acima) dentre outras técnicas que visam a performance, escalabilidade e estabilidade da aplicação.

Para entender melhor onde cada perfil pode se encaixar, temos uma tabelinha bem simples para nos ajudar a ilustrar melhor:

Arquitetura CPU/RAM OS Perfil
i586 qualquer um MS Windows Client
AMD64 qualquer um qualquer Server
64-bit SPARC qualquer um Solaris Server
32-bit SPARC 2+ cores & > 2GB RAM Solaris Server
32-bit SPARC 1 core or < 2GB RAM Solaris Client
i568 2+ cores & > 2GB RAM Linux ou Solaris Server
i568 1 core or < 2GB RAM Linux ou Solaris Client

Você pode ter mais detalhes sobre os perfís Cliente e Servidor neste link:
http://www.oracle.com./technetwork/java/hotspotfaq-138619.html#compiler_types

Caso o desenvolvedor não saiba como utilizar esses perfis, ou apenas não queira definir um, nas versões mais recentes da HotSpot existe um recurso chamado ergonomics, onde a responsabilidade de escolher o melhor perfil é da própria VM, e não só escolher entre clientserver, mas escolhe o tamanho da heap, algoritmo de coleta de lixo(GC), características baseadas no hardware e processador, o tamanho da memória e etc.

Não temos como entrar a fundo em ergonomics aqui, nosso foco é outro, então deixo com vocês algumas fontes interessantes de pesquisa para tentar entender melhor sobre isso:

Ergonomics no Java 7: http://docs.oracle.com/javase/7/docs/technotes/guides/vm/gc-ergonomics.html
Ergnomics no Java 5: http://docs.oracle.com/javase/1.5.0/docs/guide/vm/gc-ergonomics.html

Se você quer saber mais sobre como otimizar, quais parâmetros utilizar e quais os corretos para cada tipo de máquina, passo alguns links de leitura obrigatório abaixo:

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html
http://docs.oracle.com/javase/7/docs/technotes/guides/vm/server-class.html

Concluindo

Entender melhor como a VM e as técnicas utilizadas funcionam, será bem útil para o seu dia a dia. Ao saber como é feito o inline, como o JIT trabalha, como a HotSpot sabe qual método está sendo executado com frequência, irá lhe ajudar na tomada de decisões, codificação, arquitetura e estrutura da sua aplicação.

A VM do Java é bem poderosa e nos ajuda (e muitas vezes nem percebemos) a melhorar o desempenho do nosso código. Hoje na JVM existem várias outras estratégias de otimização que foram incluídas, como o mecanismo de Garbage Collector por exemplo, que é um dos mais importantes e poderosos mecanismos que a VM possui. Falamos aqui da HotSpot/JIT, que é da VM da SUN/Oracle, mas temos outras VMs de outros fornecedores, como o JRockit da BEA.

Tirar proveito ao máximo da VM é algo que podemos fazer e está em certa parte, diretamente ligada a como você codifica e faz a sua aplicação. Aprendendo de forma correta como utilizar a linguagem a seu favor, você terá esse ganho facilmente. Por isso que no curso de Java e Orientação a Objetos da TriadWorks, sempre estamos passando para os alunos essa premissa que achamos muito importante.

54 Comments on “HotSpot e JIT Compiler, entendendo como funciona!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

*

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.