15 á 24

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.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *