Checklist de perfomance — Java é lento?
Anteriormente, falei que muitos ainda dizem que Java é lento sem um argumentação consistente, ou conhecimento aprofundado em todas as tecnologias que está na comparação.
Aqui, quero descrever cada item da minha lista de verificação, para você ter uma noção de como cada item é importante, mas fique atento, pois cada item pode interferir na performance de maneira diferente, seja utilizando mais memória ou processamento que o necessário, ou ainda por estar em com problemas arquiteturais, com por exemplo estar realizando um processamento grande e síncrono bloqueando a interação do usuário até o seu termino, quando deveria estar utilizado reatividade, filas, processamento assíncrono ou multiprocessamento.
[ ] Você sabe a diferença e quando usar StringBuilder, StringBuffer e String?
Se tu vai concatenar uma String em um for-loop, prefira StringBuilder. É fácil, e tem melhor performance que StringBuffer. Mas tenha em mente, que StringBuilder não é thread-safe, logo talvez não seja uma boa em todos os casos.
StringBuilder sb = new StringBuilder(“This is a test”);for (int i=0; i<10; i++) {sb.append(i);sb.append(” “);}log.info(sb.toString())
Tenha em mente, que se concatenar uma String com sinal + (mais) no for-loop, estará sempre criando mais Strings em memória que o necessário. Lembre-se que Strings são imutáveis, você não altera, você está sempre criando uma nova.
Sabe onde vejo muito concatenação de Strings? Naquela classe que gera o SQL ou HQL dinamicamente para aquele relatório que o usuário parametriza em tela.
Mas não é sempre um problema. Olha lá em, não vamos cair na Generalização Apressada aqui no meu texto! Digamos que você está quebrando a linha de uma String para caber no seu guia de estilos, assim:
Query q = em.createQuery(“SELECT a.id, a.firstName, a.lastName ”+ “FROM Author a ”+ “WHERE a.id = :id”);
Neste caso o compilador trabalha para você, transforma em uma String unica.
[ ] Usa primitivos onde é possível?
Fácil,evita sobrecarga e melhora a performance da sua aplicação, utilize int ao invés de Integer ou double no lugar de Double. Isto faz com que sua JVM armazene o valor na stack ao invés da heap, diminui o consumo e torna o uso da memória mais eficiente.
[ ] Evita BigDecimal ou BigInteger?
As vezes pessoas decidem utilizar BigDecimal ou BigInteger pelo costume, mas estes tipos de dados cobram um preço requisitando mais memória que um simples long ou int, cálculos envolvendo BigDecimal e BigInteger é significativamente mais lento.
[ ] Você utiliza final em suas variáveis?
Quando você utiliza final em suas variáveis, você está dizendo ao GC que, enquanto o método ou classe estiver em uso, ele não deve tentar limpar tal informação da memória, otimizando, o trabalho do GC.
[ ]Você declara variáveis de classe com public static final?
Se você declarar uma String de classe com public static final estará alocando memória para esta String que nunca será liberada durante a execução da aplicação, cuidado!
[ ] Você utiliza métodos estáticos?
Você criou um método e ele é totalmente independente da classe onde ele está, o que você está esperando para tornar seu método estático?
Tornando um método estático você está dizendo para o compilador, ou interpretador, que o conteúdo do método só precisa estar na memória durante sua execução, não enquanto a classe estiver instanciada, ou seja, você libera a memória das variáveis locais após a execução do método. Além dessa maravilha, você não necessita de uma instância para executar um método estático, você invoca o método direto de sua classe, livrando sua memória de ter que armazenar atributos da classe que você não irá utilizar.
Use com moderação, não troque todos seus métodos para estático só para ganhar performance, senão o feitiço pode virar contra o feiticeiro. Métodos não estáticos tem o privilégio de compartilhar atributos, evitando de armazenar mais de uma vez a mesma informação.
[ ] Você evita atributos estáticos?
Quando você cria um atributo estático, você está criando uma variável Highlander imortal, ou seja, o endereço de memória que ela possui estará sempre ocupada, mesmo quando você não estiver usando-a.
Em algumas situações bem específicas você irá precisar de um atributo estático, pois você precisa daquela variável viva durante a execução do seu programa, mas lembre-se que com grande poderes vem grandes responsabilidades, então quantos mais você evitar esse tipo de situação, melhor.
Muita das vezes o conteúdo de um atributo estático pode ser armazenado em um arquivo ou banco de dados, eles continuarão com o mesmo comportamento e só serão usados quando necessários. Tudo isso vai depender da regra de negócio do programa.
[ ] Você evita declarar variáveis dentro de um loop?
Qual o motivo de ficar realocando memória a cada loop se você pode fazer isso apenas uma vez?
Vamos analisar o exemplo a seguir:
for (int i = 0; i < total; i++) {
Produto produto = new Produto();
produto.save(item[i]);
}
Se você não viu nenhuma falha no código acima, então olhe como deveria ficar:
Produto produto = null;
for (int i = 0; i < total; i++) {
produto = new Produto();
produto.save(item[i]);
}
Note que no primeiro exemplo é declarado uma variável produto para cada loop que ele for executado, se a variável total estiver com o valor mil, será declarado mil vezes. Já no segundo exemplo é declarado uma vez só e reutilizado dentro do loop, assim ele poderá ser instanciado várias vezes utilizando o mesmo endereço.
Bônus Com essa dica você ganha inteiramente grátis uma dica bônus, veja o exemplo a seguir:
for (int i = 0; i < total; i++) {
(new Produto()).save(item[i]);
}
No exemplo acima é feito a mesma coisa que nos outros exemplos, só que sem precisar criar uma variável. Algumas linguagens, como o Java e C#, permitem você invocar um método utilizando uma instância temporária, economizando memória.
[ ] Você evita chamar um método que calcula ou busca valores na declaração do for?
Veja que triste o código que fizemos em um dia de preguiça:
for (int i = 0; i < pedido.getTotal(); i++) {
// Código misterioso
}
Vamos deixar essa preguiça de lado e arrumar nosso código:
int total = pedido.getTotal();
for (int i = 0; i < total; i++) {
// Código misterioso
}
No segundo exemplo você criou uma variável a mais, só que você evitou que ele processasse o método getTotal() para cada interação do loop. Só olhando o código acima não é possível saber qual a complexidade do método getTotal(), e mesmo que a complexidade seja mínima, é um processamento que é desnecessário repetir neste código.
O método getTotal() poderia fácilmente, estar realizando consultas no banco de dados para retornar um simples inteiro, imagine isso sendo executado várias vezes no loop, acho que deu pra percebe o quanto isso é ruim pra performance da aplicação.
Programando como no exemplo dois, você também terá a chance de validar a variável antes que entre no loop.
Atenção Se o método getTotal() tiver a possibilidade de variar com o código interno do for, ou você está fazendo algo errado, ou você deveria estar usando o while no lugar do for.
[ ] Você conhece e utiliza imutabilidade?
Toda vez, que você precisa comparar objetos, ou collections a fim de verificar se ela mudou, você obterá melhor performance utilizando imutabilidade, ao olhar a referência de memória sabemos que se forem duas referencias diferentes, o objeto é diferente, logo gastamos menos processamento olhando cada atributo ou olhando o método equals do objeto.
Dentre os principais benefícios da Imutabilidade podemos citar a obtenção de códigos mais concisos e o fim dos efeitos colaterais (side-effects) principalmente onde thread safe é necessário, uma vez que a garantia de que uma variável não vai mudar assegura, dentro de um fluxo, que, quando o código sofrer paralelização não existirão efeitos indesejáveis como, por exemplo, modificar o objeto original ao invés de se criar um novo.
Ao analisar o SimpleDateFormat contra o FastDateFormat da Apache e o DateTimeFormatter do JodaTime, vemos que o grande calcanhar de aquiles do SimpleDateFormat é que ele não é thread-safe. Isso significa que ele não pode ser compartilhado por múltiplas threads, o que força os desenvolvedores a instanciá-lo de forma local sempre que for necessário utilizá-lo (as listagens 1 e 3 deixam claro o custo de instanciação). Devemos, portanto, ficar atentos ao usar o SimpleDateFormat em ambiente web, ou aplicações que façam uso de mais de uma thread.
A grande vantagem do DateTimeFormatter e do FastDateFormat é que ambos foram concebidos para serem classes imutáveis, e portanto, thread-safe. Isso garante que eles possam ser compartilhados em ambientes de múltiplas threads e podem inclusive serem injetados via Spring no modo Singleton, onde uma única instância pode ser compartilhada por vários outros beans do sistema, reduzindo o uso de memória.
De fato, em ambientes concorrentes, pode ser mais interessante usar o DateTimeFormatter do Joda Time, pois ele tem bom desempenho e consumo de memória. O FastDateFormat é limitado a situações onde apenas seja necessário operações de formatação.
[ ] Conhece a API e utiliza Multithreads?
Com o aumento dos núcleos disponíveis, é necessário termos conhecimento sobre como dividir o processamento, para obter mais performance.
Imagine que em um método há três chamadas a serviços (rest) diferentes, cada chamada bloquea a execução da thread corrente de forma sequencial, para depois, seguir com o processamento dos dados e então retornar o resultado.
No cenário acima, poderia abrir três threads, executa-las simultâneamente, e ao fim da execução de todas continuar o processamento.
[ ] Sabe e utiliza Set, Hash?
Saber utilizar as collections corretas, no momento correto é importante para não precisar reinventar a roda, e com isto melhorar a perfomance.
[ ] Qual a dualidade, ou regra para se implementar o métod equals e hashCode? Como equals e hashCode influênciam positiva ou negativamente no Hashtable?
se a.equals(b)
então a.hashCode() == b.hashCode()
Pra utilizar corretamente um Hastable ou HashMap você precisa saber como implementar o equals, e hashCode, ter conhecimento disto, vai fazer com que a estrutura de espalhamento de objetos realizar seu trabalho de maneira mais otimizada, e performática, inclusive evitando colisões de objetos e inconsistências.
[ ] Você utiliza reflection, gera código ou usa methodhandle?
Reflection é lento, lookup faz uma série de validações como a tipagem de dados, oque é bom do ponto de vista de que o Java faz muito do seu trabalho, mas é lento.
Você pode optar por gerar código fonte, ao invés de utilizar reflection, antigamente existia o XDoclet que era utilizado para isto junto com Paranamer para nos ajudar, hoje temos as anotações e a api de processamento de anotações que nos ajudam.
Você também pode optar por utilizar methodhandle, que permite invocar um método, como no reflection, mas a um custo muito menor, pois é como se você pegasse um ponteiro para o método, para nos dar este ganho, perdemos as validações, como de quantidade de parâmetros e tipagem, oque não deve ser um problema para quem saber oque está fazendo :D
[ ] Você utiliza Spring? Hibernate? É bom não é? É produtivo não é?
Essa comodidade e produtividade tem um custo, eles utilizam muito reflection, e o torna lento para iniciar a aplicação e lento na execução, a culpa não é do Java e sim do framework, ou do cara que escolheu utiliza-lo :D que tal tentar trocar por quarkus.io?
Em uma experiência que tive, utilizando Spring acessando SNS e SQS da AWS. Quatro profissionais observaram que o Spring era lento(Janeiro/2019), ele não lia na velocidade que precisamos, e não havia configuração para melhorar isto, acabamos implementando um algoritmo com multthreads para fazer o mesmo trabalho, e o resultado foi assustadoramente melhor.
[ ] Utiliza String.replace? Prefira StringUtils.replace do Apache commons!
Se você utiliza Java 9, String.replace até que é bom, mas se sua aplicação precisa de muitos replaces, considere utilizar o Apache Commons Lang StringUtils.replace que é bem mais rápido que a implementação de String.replcade do Java 8.
[ ] Você gerência o nível de log da sua aplicação, liga os logs quando necessário e desliga quando desnecessário?
Logs são importantes, principalmente em ambientes que você não tem acesso, já fui salvo por eles diversas vezes quando trabalhei em Curitiba, mas você deve ter atenção com os níveis de log e a maneira que concatena suas Strings.
Você vai encontrar diversos textos dizendo para você verificar se o log está habilitado para depois executar o código de log, , isto é valido, e necessário pois um if é mais barato que o processamento de uma String, mas atenção algumas bibliotecas de log, isto já é feito por padrão.
Utilize uma biblioteca que te permita mudar o nível de log da aplicação a quente, ou seja, sem precisar derrubar a a plicação, assim você pode mudar os níveis e desligar quando quiser, lembre-se que log desligado é oque lhe trará maior performance.
[ ] Utiliza logs assincronos? Sim é possível logar assincronamente sem bloquear a execução da aplicação para executar esta tarefa!
Boa parte da execução do log, é no IO, na escrita do mesmo em disco. Utilize uma biblioteca que te permita faze esta tarefa assincronamente, isto irá interferir menos na execução. Digamos que você tem um código com três linhas:
var produto = consultaProduto(10);log.debug(“produto consultado:” + produto.toString());var tabelaPreco = consultaTabelaPrecoVigente(produto);
Se cada linha demora 2 segundos pra executar, temos um total de 6 segundos. Mas se fizermos log assíncrono, talvez tenhamos 4 segundos, digo talvez pois pode variar, mas com certeza não será 6, será menos.
O contra ponto de utilizar log assincrono é que perdemos a ordem, sim, o logs poderão aparecer desordenados, por isto pode ser necessário um indexador (ELK).
[ ] Conhece e utiliza filas, Apache ActiveMQ, RabbitMQ?
RabbitMQ é um software de código aberto (open source) que foi implementado para suportar um protocolo de mensagens denominado Advanced Message Queuing Protocol (AMQP). Através desta solução, é possível criar uma aplicação para lidar com o tráfego de mensagens que estão no cerne de sistemas de informação.
A ideia do RabbitMQ é disponibilizar uma estrutura que facilite fluxos de mensagens, sobretudo em grandes aplicações, para a comunicação entre todos os processos
Imagine que temos um web service que aceita muitas requisições por segundo e também temos um sistema que processa tais requisições. Se colocarmos uma fila entre o web service e o sistema, dispomos de menos acoplamento entre as duas aplicações, porque agora ambas têm os parâmetros de configuração do gerenciador de mensagens.
Caso tenhamos muitas requisições chegando em um curto espaço de tempo, o sistema irá processar algumas requisições, mesmo se a quantidade de requisições for realmente grande. Claro que precisamos de situações mais complexas onde o número de aplicações é muito maior do que duas e que precisamos gerenciar a comunicação entre elas.
Quando você tem um monolito, que precisa começar a processar muitas requisições, por conta de um evento, digamos mensal, utilizar filas é um bom candidato para melhorar a percepção de performance do usuário.
[ ] Conhece e utiliza a programação reativa, akka, playframework, RxJava, Spring web Flux, Vert.x?
Se você não conhece o paradigma reativo corre, e aprenda, é um caminho sem volta, todos precisamos saber que existe, e claro, quando utilizar.
Há diversos materiais mostrando como utilizar o paradigma reativo melhora a percepção de performance do usuário, e a escalabilidade da aplicação.
Infelizmente, ainda temos um gargalo, que é a falta de suporte de um banco de dados relacional que suporte este paradigma. Em uma pesquisa beeeeeeemmm raza, me parece que o PostGresql tem este suporte quando falamos de utilizar este paradigma com o Vert.x.
[ ] Conhece o modelo Thread Pool? E o Event Loop?
Pois é, sabia que o modelo de Event loop, que você encontra no Vert.x, consegue atender mais requisções por segundo que o modelo de Thread Pool presente no Tomcat?
O lado negativo do Event Loop, é que se você bloquear ele sua aplicação não receberá mais requisições, enquanto que no modelo de Thread Pool, você bloqueou apenas uma requisição. Apesar que, se a aplicação continuar rodando, tende a bloquear todas as requisições também :D
[ ] Utilizando Hibernate, ao pegar um objeto com outro dentro que seja lazy, ao converter-los (os dois) quantos selects foram executados?
Se você não conhece o problema N+1 que o modelo utilizado pelo Hibernate tem, talvez você não saiba que utilizar JDBC ou iBats seja uma alternativa mais performática.
Isso, sem falar que o mapeamento objeto relacional sempre será mais lento, que trabalhar diretamente com JDBC. Mas lembre-se que estou desconsiderando questões de cache, e produtividade ao utilizar o Hibernate.
É claro que enquanto desenvolvedor entregamos mais rápido nossas tarefas utilizando JPA/Hibernate, mas esta práticidade cobra um preço, em performance, e em gasto de memória.
Você também, precisa ter noção de quem reflection é um recuso lento, a alternativa é gera código fonte no momento do build para não utilizar este recuso. Oque você acha que o Hibernate faz? Sim, ele utiliza muito reflection e manipulação do bytecode em tempo de execução.
[ ] Utilizando Hibernate, ao pegar um objeto com uma collection dentro, digamos Pessoa tem muitos PedidosDeVenda, e que você precisa processar esta collection pra realizar algum calculo, você utiliza parallelStream?
Aqui tem várias pegadinhas. A especificação do parallelStream, não diz que o processamento precisa ser realizado em paralelo, logo, também não diz quando isso poderia acontecer.
Pra priorar, o Hibernate, quando lida com uma collection, utiliza a sua implementação, que também pode decidir não faze nada em paralelo. Talvez, você esteja sendo enganado achando que está melhorando a performance da sua aplicação ao utilizar o parallelStream.
[ ] Cache de métodos, utiliza?
Em outros ecossistemas, encontramos isto com o nome de Memoize. Basicamente é apenas um cache de método, onde você irá retornar o valor do cache sempre que receber os mesmos parâmetros de entrada sem executar o método, respeitando o tempo de vida do cache, caso o cache expire o método e executado e então é alimentado o cache.
[ ] Utiliza @Lazy annotation do Spring?
O @Lazy annotation presente no Spring 3, da a possibilidade de inicializar o beam quando necessário, e não no hora de inicializar a aplicação, melhorando a sensação de performance na carga inicial.
[ ] Você utiliza o escopo mais adequado nos beans gerenciados pelo Spring?
“The singleton scope is the default scope in Spring”
Quando utilizamos Beans do Spring, é importante saber qual escopo utilizar, para mante-lo vivo em memória apenas o tempo necessário, caso o problema seja falta de memória. Se for processamento, talvez você queira deixar os beans mais tempo em memória, gastando memória e economizando processamento pra cria-lo e liberá-lo da memória.
[ ] I/O Sua aplicação faz leitua e escrita em disco?
Muitos sabem que operação de leitura e escrita em disco é lenta, por isso os provedores de nuvem não utilizam os HDs tradicionais.
Evite fazer I/O, como? Em algumas situações você pode trabalhar com mensageria, trabalhando com uma arquitetura assincrona e maneira que esta operação seja feita em um segundo momento, sem bloquear a aplicação para realizar esta operação.
[] AOT — Ahead of time compilation, você usa?
AOT é uma técnica que visa aplicar uma série de técnicas que otimizam sua aplicação, amplamente utilizada no Angular.
Lembra que falei sobre evitar o uso de introspecção (reflection)? Pois bem, com o uso de AOT você pode utilizar introspecção com a ajuda do Micronaut, ou Quarkus, tornando sua aplicação mais veloz no coldstart e consumindo menos memória.
Inicialmente você terá de abrir mão do seu framework favorito pra utilizaro Micronaut, mas isto tem mudado, está iniciando o suporte ao Spring.
Micronaut e GraalVM tem se mostrado um par bem interessante, assim como Quarkus com GraalVM, se liga!
Desde o Java 10 é possível usar o GraalVM
java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -jar …
[ ] GC, você entende as heurísticas do garbage collection?
As vezes nos code review encontro uma “explosão de strings”, boxing e unboxing, alocações de objetos de forma exagerada. A pouco tempo questionei sobre um Long que estava sendo convertido pra Integer, a resposta que tive foi: “Sempre funcionou assim”.
É importante entender como o Garbage Collector funciona. É importante conhecer suas principais heurísticas e pensar alocação de acordo com o especificado.
De forma breve, são boas abordagens:
- evitar alocações (quanto menos melhor)
- alocar objetos que serão úteis durante muito tempo (reaproveitamento)
- alocar (não muitos) objetos que podem ser desalocados rapidamente
- Alguns padrões de projeto permitem reaproveitar objetos, como o flyweight pattern, você o conhece?
O Garbage Collector é fantástico. Ele permite que nossas aplicações tenham ótima performance (muitas vezes, superiores a aplicações não gerenciadas), […], mas isso só será possível se nosso código se comportar de acordo com suas regras. Do contrário, “GC will kill you” (Oren Eini)
[ ] AWS Lambda em Java é lento para inicializar
Talvez você deva considerar não utilizar AWS Lambda, caso o processo seja executado sempre, digamos durante as 24 horas do dia é provável que o custo seja maior que criar uma aplicação que faça a mesma coisa.
Talvez você deva conhecer o Quarkus, que vem pra melhorar e muito esta situação entre outros ganhos relacionados a cloud.
Não está convencido? Da uma olhada também nestes vídeos….
Lições
- Não faça generalização apressada
- Domine as tecnologias, no nível baixo de engenharia
- Utilize o checklist do Clayton para otimizar sua aplicação Java ;)
- Vários itens do meu checklist são agnósticos a linguagem, podem ser utilizados até mesmo no front em Angular.
- Não faça otimização prematura, você não precisa ser o chato escovador de bits
Pra finalizar quero adicionar mais um item a lista, código limpo. Todo código sujo tende a ser mais lento por realizar processamento desnecessário, com exceção daquele código que escova bits é claro.
Recomendo a leitura deste livro, ele trás diversas informações importantes quanto a este assunto
Java Efetivo (Português) 2º edição
Effective Java (3rd Edition) (Inglês)
Você conhece algo que possa ajudar a deixar minhas aplicações escritas em Java mais rápido? Comenta ai! Quero aprender com tigo também!