Garbage Collection em Java (2)
Publicado em 05/01/2015
Este é o segundo de uma série de artigos sobre Garbage Collection em Java, traduzidos dos originais publicados por Richard Warburton no seu blog. Richard é autor do livro da editora O'Reilly Java 8 Lambdas. (Tradução e publicação autorizadas pelo autor.)
Parallel Scavenge (Limpador em Paralelo)
Examinaremos hoje como trabalham os Garbage Collectors em Paralelo. Especificamente, isso é uma combinação da execução do coletor Parallel Scavenge sobre o Eden e do coletor Mark and Sweep sobre a geração em Tenured. Pode-se obter esta opção passando o parâmetro: -XX:+UseParallelOldGC
, embora ele já seja o padrão em certos tipos de máquinas.
Talvez você queira ler meu primeiro artigo sobre Garbage Collection, se ainda não o fez, pois ele dá uma visão geral do assunto.
Espaços Eden e Survivor
No coletor parallel scavenge os espaços Eden e Survivor são coletados usando uma abordagem conhecida como GC Hemisférica. Os objetos são inicialmente alocados no Eden e, quando ele está quase cheio1, uma GC do espaço Eden é disparada. Isso identifica os objetos vivos e os copia para o espaço Survivor ativo2. Ele então trata todo o espaço Eden como um bloco de memória livre e contínuo que pode ser alocado novamente.
Neste caso o processo de alocação fica parecido com o corte de um pedaço de cheddar. Cada pedaço é cortado de forma contígua e então a fatia seguinte será a próxima a ser “comida”. Isto tem a vantagem de que a alocação requer apenas uma adição de ponteiros.
A fim de identificar objetos vivos é realizada uma busca no grafo de objetos. A busca começa por um conjunto de objetos “raiz” que são objetos certamente vivos; por exemplo, cada thread é um objeto-raiz. A busca encontra então objetos que são apontados por um conjunto raiz, e se expande para fora até encontrar todos os objetos vivos. Abaixo está uma bela representação gráfica, cortesia de Michael Triana:
Paralelo, no contexto de parallel scavenge significa que a coleta é feita por múltiplas threads executadas ao mesmo tempo. Isso não deve ser confundido com a GC incremental, na qual o coletor é executado simultaneamente ou intercalado com o programa. A coleta em paralelo melhora o desempenho geral da GC, usando com mais eficiência as modernas CPU multicore. O paralelismo é alcançado dando a cada thread um conjunto de raízes para marcar, e um segmento da tabela de objetos.
Há dois espaços Survivor, mas apenas um deles está ativo num determinado momento. Eles são coletados da mesma forma que no Eden. A ideia é que os objetos sejam copiados no espaço Survivor ativo quando eles são promovidos do Eden. Então, quando chega a hora de esvaziar o espaço, eles são copiados no espaço Survivor inativo. Assim que o espaço Survivor estiver completamente vazio o espaço inativo torna-se ativo, e o espaço ativo torna-se inativo. Consegue-se isso invertendo-se o ponteiro para o começo do espaço Survivor e significa que todos os objetos mortos neste espaço podem ser liberados, bastando tão somente uma atribuição de ponteiro.
Concepção e compensações de tempo da "Young Gen"
Uma vez que isso envolve apenas a cópia de objetos vivos e mudanças de ponteiros, o tempo levado para coletar os espaços Eden e Survivor é proporcional à quantidade de objetos vivos. Isso é muito importante uma vez que, de acordo com a hipótese geracional, sabemos que a maioria dos objetos morrem jovens, e assim não há nenhum custo para os GC liberarem a memória associada a eles.
A concepção dos espaços Survivor é motivada pela ideia de que a coleta de objetos quando são jovens fica mais em conta do que uma coleta no espaço Tenured. A coleta contínua de objetos de uma forma hemisférica por algumas execuções de GC ajuda no desempenho geral.
Finalmente, o fato de que o Eden é organizado num único espaço contíguo, torna a alocação pouco custosa. Um programa em C pode usar um comando malloc
para alocar um bloco de memória, o que implica em percorrer uma lista de espaços livres na memória tentando encontrar algum que seja grande o suficiente. Quando se usa um alocador de área e se aloca consecutivamente, tudo que se precisa fazer é verificar se há espaço livre suficiente e então incrementar um ponteiro de acordo com o tamanho do objeto.
Mark and Sweep em Paralelo
Os objetos que tenham sobrevivido a um certo número de coletas habilitam-se ao espaço Tenured. O número de vezes que precisam sobreviver é o 'limiar de tenuring'. As coletas em Tenured operam um pouco diferentemente das feitas no Eden, usando um algorítimo chamado mark and sweep (marca e limpa). Cada objeto tem um bit de marcação associado a ele. Todas as marcas são inicialmente configuradas como falsas e, a medida em que o objeto é alcançado durante a busca no grafo, são configuradas como verdadeiras.
A busca no grafo que identifica objetos vivos é similar à busca descrita para geração jovem. A diferença é que ao invés de copiar objetos vivos, ela simplesmente os marca. Depois disso ela pode percorrer a tabela de objetos e liberar qualquer um que não esteja vivo. Este processo é feito em paralelo em diversas threads, cada uma pesquisando uma região da heap.
Infelizmente, este processo de deletar objetos que ainda estão vivos deixa o espaço Tenured parecendo um queijo suiço. Resta alguma memória em uso na qual os objetos ainda estão vivos, entremeada de buracos que outros ocupavam. Este tipo de fragmentação é prejudicial para o desempenho do aplicativo porque torna impossível alocar objetos que sejam maiores do que o tamanho dos buracos.
A fim de reduzir o problema do queijo suiço a Mark/Sweep em Paralelo compacta a heap tentando deixar os objetos vivos alocados contiguamente no começo do espaço Tenured. Depois da deleção ele procura áreas do espaço Tenured para identificar quais tem baixa e alta taxas de ocupação. Os objetos vivos das regiões de baixa ocupação são movidos para baixo em direção às regiões que tem alta ocupação, as quais estarão naturalmente na extremidade mais baixa da memória em razão da prévia fase de compactação. A movimentação de objetos nesta fase, na verdade, é executada pela thread alocada para a região de destino, ao invés da região de origem.
Resumo
- O Parallel Scavenge divide a heap em quatro espaços: Eden, dois Survivors e Tenured.
- O ** Parallel Scavenge** usa um coletor de cópia em paralelo para coletar os espaços Eden e Survivor.
- Um algorítimo diferente é usado para o espaço Tenured. Ele marca todos os objetos vivos, deleta os objetos mortos e então compacta o espaço.
- O Parallel Scavenge tem um bom desempenho, mas ele interrompe o programa quando é executado.
Na terceira parte desta série, examinarei como funciona a CMS, ou Concurrent-Mark-Sweep. Felizmente, será um artigo de leitura mais amena para aqueles com alergia à lactose.
Notas
1 Tecnicamente há um “limiar de ocupação” para cada espaço na heap, o qual define quão cheio o espaço pode ficar antes que ocorra a GC.
2 Este algorítimo de cópia é baseado no algorítimo de Cheney.