Melhore a performance do seu código Java
A performance em Java sempre foi uma preocupação de muitos desenvolvedores, e tem melhorado significativamente desde o surgimento da linguagem. Ainda assim, algumas técnicas simples podem aumentar, e muito, a performance do seu código em Java.
Introdução
Programamos em Java, muitas vezes, sem nos preocupar muito com a performance. Claro que sempre devemos evitar algoritmos menos inteligentes, sempre buscando uma ordem de magnitude melhor. Se o seu algoritmo demora 10T, não será um ganho expressivo isto mudar para 5T em Java, já que uma aplicação corporativa, escalabilidade e confiança são pontos muito mais importantes. Mas uma prática muito comum para programadores de C e C++, é buscar a performance fora do escopo de magnitude.
Este artigo visa mostrar algumas opções simples, que podem te ajudar muito a evitar os maiores gargalos da virtual machine, e em especial do garbage collector. Mas lembre-se que estes padrões devem ser apenas utilizados em extrema necessidade. Alguns desses códigos são hostis para o futuro leitor do seu código, e podem não funcionar direito a medida que as VMs evoluem. Vamos começar com as técnicas de melhoria que são aconselhadas.
Concatenação de strings
Este aqui dispensa apresentações! Quando você utiliza o operador “+”, para concatenar Strings, uma quantidade enorme de novos objetos são instanciados:
String temp = "";
for (int i = 0; i < 100000; i++) {
temp += "string";
}
Neste exemplo, você pode não ter percebido, mas alocou 1000 objetos no heap, que tem vida apenas dentro do for. Objetos foram criados por causa das concatenações parciais!
Se a solução fosse esta:
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < 100000; i++) {
buffer.append("string");
}
String temp = buffer.toString();
Nenhum objeto é criado a mais, apenas o StringBuffer. Se você fizer um teste assim, e marcar o tempo de execução, verá que a diferença é mais que gritante. Isso sem considerar que o garbage collector irá usar um tempo considerável coletando todas aquelas strings temporárias.
É claro que você não precisa utilizar a solução do StringBuffer para concatenar a String que vai aparecer como mensagem da exceção. Você deve utilizar isto especialmente nos loops mais internos do seu algortimo.
Cópia de arrays em coleções
Quando você utiliza alguma coleções, do Collection framework, os seus objetos ficam armazenados em uma array.
Uma array em Java, tem tamanho fixo, e este tamanho não pode ser mudado! Então, o que acontece quando uma coleção sua atinge o tamanho máximo daquela array? A coleção cria uma nova array, de tamanho maior, e copia tudo da velha para a nova. Isto é uma operação relativamente demorada.
Você pode passar para o construtor da coleção, o tamanho inicial dela, e juntamente com isso, a incrementação do tamanho de array. Isto é, quando a sua array estoura, qual o tamanho que a nova array deve ter? Sugiro você manter esses números elevados. O HashMap tem este construtor, mas a ArrayList só tem o construtor que você passa o tamanho inicial.
Vamos ver um exemplo da ArrayList:
ArrayList al = new ArrayList();
Isto cria uma ArrayList de tamanho 10! Se você for inserir 1000 elementos nela, ela vai ficar se expandindo inúmeras vezes (a expansão do arraylista é de 150% a cada estouro de tamanho). Neste caso, a seqüência de incrementos será: 100, 150, 225, 338, 508, 761, 1141. Isto é, 6 System.arraycopy() totalmente desnecessários!
Você deve sempre utilizar o construtor que recebe o tamanho da coleção!
Para um HashMap:
HashMap hm = new HashMap(1000, 2);
Indica que eu quero um hashmap de tamanho 1000 inicialmente, e que quando estourar, passe para 2000 chaves.
Isto também é válido durante a criação de um StringBuffer. Muito cuidado! Os seus appends podem ficar muito mais rápidos se você tiver uma boa estimativa de quão grande o seu Buffer pode chegar. Arredonde para cima!
Chamada de método virtual
Esta aqui vai assustar muita gente!
Quando você possui uma referência a um objeto, e chama um método não estático dele, o Java tem de procurar qual método invocar. Isto não é instantâneo, já que você pode ter um Object apontando para uma String, e se você chamar o toString deste objeto, ele tem de chamar o toString do String.
Vejamos:
public class Teste {
public String toString() {
return "ola";
}
public static void main (String args[]) {
long x = System.currentTimeMillis();
Object o = new Teste();
for (int i = 0; i < 5000000; i++) {
o.toString();
}
long t = System.currentTimeMillis() - x;
System.out.println("Tempo total: " + t);
}
}
No meu micro, demorou 79 milisegundos em média.
Agora, vamos nos referenciar a este objeto da maneira mais exata possível:
public class Teste {
public String toString() {
return "ola";
}
public static void main (String args[]) {
long x = System.currentTimeMillis();
// referenciando especificamente!
Teste t = new Teste();
for (int i = 0; i < 5000000; i++) {
t.toString();
}
long t = System.currentTimeMillis() - x;
System.out.println("Tempo total: " + t);
}
}
Demorou 21 segundos! Quase 400% de diferença! para uma hierarquia maior de classes, este tempo aumenta mais ainda! Você pode apostar que tem milhares de linhas do seu código em que você poderia mudar isso!
Mantenha sempre sua referência a mais específica possível. Fiz o mesmo teste acima, utilizando um casting, e a medição continuou em 21 milisegundos.
Técnicas avançadas e não aconselhadas para aumentar a performace
Antes de começar este tópico, a própria Sun desaconselha totalmente essas práticas, especialmente porque algo pode mudar no hotspot ou garbage collector de uma implementação pra outra, deixando o seu código mais lento do que sem esse “tuning”.
Você só deve utilizar destas técnicas se tiver absoluta necessidade de performace.
Warming up dos métodos detectados pelo Hot Spot
O Hotspot demora um pouco até perceber que um determinado laço é “hot” (isto é, utilizado muito), para então poder pedir ao JIT que compile este trecho de código.
Muitas pessoas adotam a seguinte medida: Antes da aplicação realmente começar, durante a inicialização, você faz inúmeras chamadas ao método que você precisa que seja compilado pelo JIT. Com isso, você sugere ao hotspot que este método deve ser compilado, e quando você realmente precisar dele, ele já estará rodando mais rápido!
Precisa falar que isso é realmente não aconselhado?
Pool de objetos
Uma prática que tem se tornado cada vez mais comum, para evitar o estresse da memória Heap.
Toda vez que você chama o operador new, um bom tempo é gasto para alocar memória no Heap e te retornar uma referência. Se contar que o construtor do seu objeto pode, por sua vez, construir mais outros objetos, alocando no Heap novamente.
Para evitar esse tipo de alocação e desalocação, você cria uma classe para gerenciar os seus objetos que são muito instanciados:
// deveria ser um singleton ao invés de tudo estáticopublic Class CarroPool { private ArrayList carros = new ArrayList(1000); public static Carro newCarro(String cor) { // nao tem carro na pool! if (carros.size() == 0) { return new Carro(cor); } // tem carro na pool // vamos reutilizar memoria Carro c = (Carro) al.remove(0); c.setCor(cor); return c; } public static void releaseCarro(Carro c) { carros.add(c); } }
Então, você cria os seus objetos pelo pool:
Carro c = CarroPool.newCarro("azul");
// depois de usar:
CarroPool.releaseCarro(c);
//Tenha o finalize() sempre devolvendo o objeto ao pool, mas prefira sempre fazer o release na mão. Cuidado para não ficar dependendo muito do Garbage Collector.
public class Carro {
...
protect void finalize() {
CarroPool.releaseCarro(this);
}
}
Você pode deixar o seu pool muito avançado, acoplando sincronização para não alocar memória demais, e esperar por uma instância retornar ao pool, ao invés de criar uma nova quando ela está vazia!
Fine tuning da Virtual Machine
A Virtual Machine que a Sun disponibiliza, possui uma série de variáveis que pode ser optimizadas para o seu uso. Algumas delas são padrões, e todas as virtual machines devem suportar. Algumas outras são dependentes de plataforma, e você não é aconselhado a usar de jeito nenhum!
Você pode, por exemplo, especificar quando o Garbage Collector deve agir. Isto é, você pode falar algo como: só execute quando a memória estiver 50% cheia. Este tipo de ação é ruim, pois quebra objetos que deveriam liberar outros tipos de recursos que não são memória: arquivos e conexões com banco de dados são os mais comuns.
Você pode encontrar todas as opções que a virtual machine da Sun te oferece, no link:
http://java.sun.com/docs/hotspot/VMOptions.html
Infelizmente estas opções estão mal documentadas.
Conclusão
Evite depender desse tipo de implementação. Isto serve apenas para mostrar que o código Java realmente pode ser mudado afim de alcançar performance. Dominar o funcionamento do Hotspot e do Garbage Collector diferenciam um bom programador Java de um programador com domínio em Java!
Links interessantes:
. Fazendo o fine tuning em alguns simples programas;
. Opções de linha de comando na chamada da Virtual Machine;
. Saiba mais sobre a Virtual Machine, Hotspot e Garbage Collector, em nível de implementação;
. Perguntas e respostas sobre o Hotspot;
. Perguntas e respostas infrequestes sobre Java (um pouco antigo);
. Especificação da Virtual Machine;
. Dicas sobre performance no Java (super desorganizado).
Se gostou da matéria deixe um comentário or subscribe to the feed and get future articles delivered to your feed reader.





Mt bom seu artigo. Valeu, sou programador Java a pouco tempo e as informações foram muito valiosas.
Valeu!