Com a JVM no centro da Plataforma Java, conhecer seu funcionamento interno é essencial para qualquer aplicação Java.
Dentre os diversos tópicos associados à JVM, destacamos alguns que julgamos vitais para todo desenvolvedor Java.
Durante muito tempo, uma das maiores dificuldades na hora de programar era o gerenciamento de memória.
Hoje, em todas as plataformas modernas, Java inclusive, temos gerenciamento de memória automático através de algoritmos de coleta de lixo.
O Garbage Collector (GC) é um dos principais componentes da JVM e responsável pela liberação da memória que não esteja mais sendo utilizada.
Mas não conseguimos determinar o momento exato em que essa coleta ocorrerá; isto depende totalmente do algoritmo do garbage collector.
Em geral, o GC não fará coletas para cada objeto liberado; ele deixará o lixo acumular um pouco para fazer coletas maiores, de maneira a otimizar o tempo gasto.
Essa abordagem, muitas vezes, é bem mais eficiente, além de evitar a fragmentação da memória, que poderia aparecer no caso de um programa que aloque e libere a memória de maneira ingênua.
Em geral, a primeira ideia que aparece ao se pensar em GC é que ele fica varrendo a memória periodicamente e libera aqueles objetos que estão sem referência.
Esse algoritmo envelheceu, da mesma forma que o ingênuo reference counting. Estudos extensivos com várias aplicações e seus comportamentos em tempo de execução ajudaram a formar premissas essenciais para algoritmos modernos de GC.
Com base nessas observações, chegou-se ao que hoje é conhecido como o algoritmo generational copying, usado como base na maioria das máquinas virtuais.
É simples observar esse padrão geracional em muitos programas escritos em Java, quando objetos são criados dentro de um método.
Assim que o método termina, alguns objetos que foram criados lá ficam sem referências e se tornam elegíveis à coleta de lixo, isto é, eles sobreviveram apenas durante a execução do método e tiveram vida curta.
Mesmo métodos curtos e simples, como toString, acabam gerando objetos intermediários que rapidamente não serão mais referenciados: public String toString() { return “[ contatos: “ + listaDeContatos + “]”; Java Virtual Machine } Aqui, durante a concatenação das três partes da String, um StringBuilder será utilizado, e o mesmo vai ocorrer para a invocação implícita do toString da coleção listaDeContatos, que gera uma String a partir de outro StringBuilder.
Os objetos que sobrevivem à coleta são, então, copiados para a geração seguinte, e todo o espaço da geração nova é considerado disponível novamente.
Esse processo de cópia de objetos sobreviventes é que dá nome ao algoritmo.
Mas seu grande trunfo é que ele age nos objetos sobreviventes, e não nos descartados, como faria um algoritmo tradicional.
No descarte, os objetos não são verdadeiramente apagados da memória; o GC apenas marca a memória como disponível.
E, embora uma cópia seja relativamente custosa, copiar apenas os poucos sobreviventes é mais rápido que liberar, um por um, os diversos objetos mortos.
Novos objetos são alocados na young e, assim que ela estiver lotada, é efetuado o chamado minor collect.
Major collects são também chamados FullGC, e costumam demorar bem mais, já que varrem toda a memória, chegando a travar a aplicação nos algoritmos mais tradicionais (não paralelos). É possível fazer uma observação sucinta do comportamento do GC mesmo sem um profiler, bastando usar a opção -verbose:gc ao iniciar a JVM.
É importante observar esses valores para perceber se o programa não está gastando muito tempo nos GCs, ou se as coletas estão sendo ineficientes.
Na verdade, como os algoritmos estão adaptados segundo a hipótese das gerações, o melhor são muitos pequenos objetos que logo se tornam desnecessários, do que poucos que demoram para sair da memória.
Em alguns casos, até o tamanho do objeto pode influenciar; na JRockit, por exemplo, objetos grandes são alocados direto na old generation , logo não participam da cópia geracional.
A melhor técnica que um desenvolvedor pode utilizar é encaixar a demanda de memória da sua aplicação na hipótese das gerações e nas boas práticas de orientação a objetos, criando objetos pequenos e encapsulados de acordo com sua necessidade.
Se o custo de criação do objeto não for grande, segurar suas referências ou fazer caches acaba sendo pior.
Obviamente, isso exclui casos em que o custo de criação é grande, como um laço de concatenação de String através do operador +; nesse caso, é melhor usar StringBuilders ou StringBuffers.
Algoritmos ingênuos de GC costumam causar grande fragmentacão, porque apenas removem os objetos não mais usados, e os sobreviventes acabam espalhados e cercados de áreas vazias.
O generational copying copia os objetos sobreviventes para outra geração de forma agrupada, e a memória da geração anterior é liberada em um grande e único bloco, sem fragmentação. Fora isso, outras estratégias de compactação de memória ainda podem ser usadas pela JVM, inclusive na old generation.
É importante notar que isso só é possível por causa do modelo de memória do Java, que abstrai totalmente do programa a forma como os ponteiros .
É possível mudar objetos de lugar a qualquer momento, e a VM precisa apenas atualizar seus ponteiros internos, o que seria muito difícil de realizar em um ambiente com acesso direto a ponteiros de memória.
A primeira especifica o tamanho inicial do heap, e a segunda, o tamanho máximo. Inicialmente, a JVM aloca no sistema operacional a quantidade Xms de memória de uma vez, e essa memória nunca é devolvida para o sistema.
A alocação de memória para os objetos Java é resolvida dentro da própria JVM, e não no sistema operacional.
Conforme mais memória é necessária, a JVM aloca em grandes blocos até o máximo do Xmx (se precisar de mais que isso, um OutOfMemoryError é lançado).
É muito comum rodar a máquina virtual com valores iguais de Xms e Xmx, fazendo com que a VM aloque memória no sistema operacional apenas no início, deixando de depender do comportamento específico do SO.
Conhecer essas e outras opções do garbage collector da sua JVM pode impactar bastante na performance de uma aplicação.