23 de abr. de 2010

Java Memory Architecture

Salve

Falando um pouco sobre detalhes de baixo nível em Java, é bastante curioso o modo como a memória é gerenciada no Java, e neste pequeno texto vou expor um pouco do que sei a respeito.


Coleção de Lixo

Java é uma linguagem orientada a objetos (não totalmente, existem tipos primitivos em Java) que utiliza gerenciamento automático de memória, um recurso conhecido como Coleção de Lixo (Garbage Collection). Isto quer dizer, que não é necessário desalocar o recurso manualmente, como uma referência para um objeto, eliminando boa parte do trabalho que isto exige (que diga os desenvolvedores C/C++).

Entretanto, não entenda que é desnecessário preocupar-se com o gerenciamento de memória, porque É!
Vários problemas podem acontecer devido ao descuido e desenvolvimento "despreocupado" de alguns programadores, como o OutOfMemoryError (OOME) eStackOverflow. Entre os OOME presenciamos com frequência o PemGen Space.



Algoritmos de Coleta de Lixo

Não é somente o Java que usa o mecanismo de coleta, outras linguagens e plataformas como .NET, Ruby, etc, também utilizam a mesma técnica. As preocupações mais relevantes são:

  • Quantos objetos podem recolhidos e quanta memória pode ser desalocada a cada coleta: Existe um intervalo de tempo para o coletor de lixo agir, e cada ação é importante que ela seja eficiente;
  • Tempo necessário para coleta: Enquanto o coletor de lixo está agindo, alguns bloqueio de memória são impostos e recursos como o processador são muito utilizados, podendo fazer até a aplicação para de responder por um pequeno intervalo de tempo;
  • Aproveitamento do multiprocessamento: A coleta de lixo pode tirar proveito do paralelismo (SMP) disponível em servidores, clusters (né Guilherme?) e até estações de trabalho e outras plataformas com mais de um núcleo de processamento.

Existem várias técnicas para gerenciar a memória usando a coleta de lixo, entre elas:

- Mark and Sweep: Os objetos referenciados são marcados em uma primeira passada e em outra passada subsequente todos os objetos não marcados são recolhidos. Este tipo de coleta demanda muito processamento, e por isso é conhecido como um algoritmo Stop The World (para o mundo), causando algunsSlow Downs (lentidões na resposta da aplicação).

- Reference Counting: Neste caso a alocação de objetos é monitorada e as referências, como o próprio nome diz, são contadas. Objetos sem referências, dado um intervalo de tempo, são coletados.

- Generational Copying: Este último é o mais empregado, e em uso nas implementações atuais da JVM. O espaço de memória é divido em gerações jovem (young generation) e velha (old generation). A maioria dos objetos tem vida curta, por exemplo, quando são instanciados no corpo de um método, então, por padrão, os objetos são alocados em um espaço de memória menor (young generation), que é limpo sempre que alcança o limite e os objetos sem referência são desalocados, e os sobreviventes, teoricamente são os de vida longa e são movidos para um espaço maior, onde a coleta é menos freqüente (old generation).



Arquitetura da memória na Java

A plataforma Java mantém dois espaços bem claros para alocação, a pilha (stack), onde ficam as variáveis, métodos e o carregador de classes (Class Loader), e o heap, onde vivem os objetos.

+----------+ +--------------------------------+
|          | |                                |
|  PILHA   | |             HEAP               |
|          | |                                |
+----------+ +--------------------------------+

O Heap é divido em três espaços bem definidos:

+----------+---------------------+------------+
|  YOUNG   |        OLD          | PERMANENT  |
|GENERATION|     GENERATION      | GENERATION |
|          |                     |            |
+----------+---------------------+------------+

Os objetos recém criados são armazenados na Young Generation (chamado de Eden na implementação HotSpot), onde são feitas coletas mais frequentes, chamadas de Minor Collects

Os objetos de vida longa são movidos para Old Generation, onde as coletas são menos frequentes, mas que liberam mais memória sempre que são realizadas, pois ali residem objetos maiores, sendo chamadas de Major Collects

O espaço de geração permanente é responsável por manter o pool de Strings (instâncias de Strings são reusadas, fica para outro tópico) e classes (Classes são objetos).



Parâmetros da JVM e Tunning

Para alterar o espaço reservado para alocação de memória é comum usarmos parâmetros como os que seguem abaixo:

java aplicacao -Xms768M -Xmx1024M

Quer dizer que inicialmente 768 Mega Bytes serão reservados para a JVM, e que poderá expandir até 1 Giga Byte. Note que este espaço máximo é da Young + Old Generation e não inclui o Permanent Generation:




+----------+--------------------+------------+
|  YOUNG   |        OLD         | PERMANENT  |
|GENERATION|     GENERATION     | GENERATION |
|          |                    |            |
+----------+--------------------+------------+

|--------  1024 MB -------------|

Para alterar o tamanho máximo da Permanent Generation é necessário adicionar o parâmetro -XX:MaxPermSize=128M (exemplo para 128MB). Logo o parâmetro abaixo usaria 1,5GB da memória disponível:

java aplicacao -Xms768M -Xmx1024M -XX:MaxPermSize=512M




+----------+--------------------+------------+
|  YOUNG   |        OLD         | PERMANENT  |
|GENERATION|     GENERATION     | GENERATION |
|          |                    |            |
+----------+--------------------+------------+

|--------  1024 MB -------------|-- 512MB ---|
|------------ 1536MB (1.5GB) ----------------|


Para otimizar a coleta, é possível alterar o algoritmo usado para a coleta. Por padrão, a Máquina Virtual usa o Serial Collector, que é bem eficiente, mas consiste em uma thread em background, responsável pela coleta, o que é bom para uma ambiente com poucos processadores disponíveis, mas que deixa de aproveitar o multiprocessamento disponível em alguns ambientes. Para habilitá-lo explicitamente é usado o seguinte parâmetro:

java aplicacao -XX:-UseSerialGC

Para ambientes multiprocessados, é interessante usar outra estratégia, que tire proveito do paralelismo. O algoritmo Parallel Garbage Collector faz isso, e para habilitá-lo, parametrize da seguinte maneira:

java aplicacao -XX:-UseParallelGC

ParallelGC consegue fazer vários Minor Collects em paralelo, sendo especialmente otimizado para plataformas multiprocessadas e aplicações com grande utilização de objetos de vida curta (pequeno escopo).

Outro algoritmo, que causa menos slow downs é o Concurrent Mark and Sweep. É especialmente interessante para Major Collects, usado em máquina com multiprocessamento e grande volume de memória disponível. Para usar:

java aplicacao -XX:-UseConcMarkSweep

Note, para acompanhar as coleções realizadas adicione o parâmetro -verbose:gc

A saída é algo como:




[GC 25066K->24710K(62848K), 0.0850880 secs]
[GC 28201K(62848K), 0.0079745 secs]
[GC 39883K->31344K(62848K), 0.0949824 secs]
[GC 46580K->37787K(62848K), 0.0950039 secs]
[Full GC 53044K->9816K(62848K), 0.1182727 secs]
....

É possível ver os Minor Collects e os Major Collects agindo, a memória desalocada e o tempo despendido em cada um.



Mais informações

Uma lista completa de parâmetros que podem ser usados para tunning está aqui:

Ótima leitura:

Recentemente a SUN está trabalhando em um algoritmo experimental chamado Garbage First (G1) que divide a memória em áreas menores e promete reduzir o tempo de coleta, além de possuir várias opções de parametrização:

Acho que já é suficiente para uma boa leitura. Abraço a todos, e podem comentar, criticar e perguntas a vontade.

Nenhum comentário: