Algumas das boas práticas mostradas aqui podem parecer micro-otimização, otimização prematura ou otimização de fatores constantes. Mas desempenho e desperdício de memória em milhares destes pequenos lugares acabam se somando rapidamente e irão "moer" uma aplicação até ela "rastejar". Aplicação aqui refere-se a uma aplicação que rode no lado servidor, dentro de um servidor de aplicação. Em aplicações desktop, as situações mencionadas aqui podem não ser tão ruins. Mas então, qual é a única plataforma relevante que roda aplicações Java no lado cliente? Android. Uma plataforma embarcada com recursos bem limitados como memória por exemplo. Neste caso, mesmo a otimização de fatores constantes valem a pena. Iterar sobre arrays em vez de listas é um exemplo.

Se você está interessado em como programar otimizado para o compilador, leia a JDK Performance Wiki (em inglês).

No fim das contas, muito do desempenho da aplicação vai depender da qualidade geral do seu código-fonte. A propósito: nunca subestime a importância das "pegadas" de memória deixadas para trás. Muitas aplicações apresentam problemas de desempenho devido a um grande overhead do Garbage Collector e erros Out of Memory. Mesmo que o Garbage Collector seja bastante rápido, a maior parte da escalabilidade de um código do lado servidor é determinada por dois números:

Reduzir ambos os números por meio da otimização de um fator constante irá diretamente aumentar a vazão.

Considere estes cenários (assumindo uma young generation de 100MB):

Cenário Threads Duração da Requisição (ms) Memória usada por Requisição (KB) Máximo de Requisições por segundo Memória usada por segundo (KB) Coletas por segundo com Young Generation = 100MB
Base 30 100 50 300 15000 0,15
Mais Lento 30 1000 50 30 1500 0,015
Mais Memória 30 100 500 300 150000 1,5
Excesso de Memória 30 100 5000 300 1500000 15

A seguir, cada cenário foi representado em gráfico onde o eixo X é o tempo transcorrido em milissegundos ao longo de 2 segundos e o eixo Y é a memória alocada. Em azul está a memória alocada ainda em uso e em vermelho a memória alocada não mais usada (lixo que pode ser coletado). O raio representa o evento de coleta de lixo realizado pelo Garbage Collector. Todos os gráficos estão na mesma resolução para permitir a comparação visual direta entre eles. O momento exato da coleta e como a memória usada é liberada pode não estar graficamente preciso e possui mais caráter ilustrativo.

Legenda.png

Base.png

Este cenário 'Base' serve de comparação para os demais. Dentro do período representado de 2 segundos não chegou a ocorrer o evento de coleta de lixo por que a young generation não atingiu os 100MB.

Mais Lento.png

No cenário 'Mais Lento' a duração da requisição é 10 vezes maior. Isto imediatamente reduz o número máximo de requisições por segundo na proporção de 10 também.

Mais Memória.png

No cenário 'Mais Memória', cada requisição usa 10 vezes mais memória. Isto aumenta diretamente o número de coleta de lixo para mais de 1 por segundo, o que causa um overhead que não pode ser negligenciado.

Excesso de Memória.png

Usar muito mais memória como no cenário 'Excesso de Memória' pode levar a 15 coletas por segundo, deixando apenas 66 milissegundos por coleta, o que claramente não é suficiente. A sistema irá degradar-se seriamente. O tempo de 66 milissegundos é abaixo do tempo da duração da requisição que é de 100 milissegundos. Então muitas requisições em atendimento serão mantidas na memória, não podendo ser coletadas, e causando uma propagação dessa memória para gerações posteriores. Isto significa que as gerações mais velhas irão começar a crescer e irão precisar de uma grande (e consequentemente lenta) coleta em breve. A aplicação neste cenário não tem mais desempenho razoável.

Espero que isto tenha mostrado claramente o quanto ruim é um código com excesso de consumo de memória quando comparado a um código apenas lento. Todo o seu código super-rápido não poder ajudá-lo quando você aloca muita memória.

E agora, vamos ver alguns exemplos ruins de código?