Este é o segundo de uma série de artigos sobre Garbage Collection em Java, traduzidos dos originais publicados por Richard Warburton no seu blog. (Tradução e publicação autorizadas pelo autor.)

Esta é a continuação dos meus dois artigos anteriores sobre coleta de lixo (garbage collection):

  1. Visão geral da GC na HotSpot.
  2. Garbage Collectors em Paralelo.

Marcação e Limpeza Concorrentes (Concurrent Mark Sweep)

Os coletores de lixo paralelos na Hotspot foram projetados para minimizar a quantidade de tempo que o aplicativo gasta efetuando coleta de lixo, denominado throughput. Esta não é uma solução adequada para todos os aplicativos — alguns exigem que as pausas individuais também sejam curtas, o que é conhecido como exigência de latência.

O coletor de marcação e limpeza concorrente (Concurrent Mark Sweep — CMS) é projetado para ser de latência mais baixa do que os coletores paralelos. O ponto chave do seu projeto é tentar fazer uma parte da coleta de lixo ao mesmo tempo em que o aplicativo está sendo executado. O resultado é que, quando o coletor precisa suspender a execução do aplicativo, não o faz por um período de tempo muito longo.

Neste ponto você provavelmente deve estar se perguntando: "mas afinal, 'paralelo' e 'concorrente' não querem dizer a mesma coisa?" Bem, no contexto da GC, paralelo significa "usar múltiplas threads para executar a GC ao mesmo tempo" e concorrente significa que "a GC é executada simultaneamente com o aplicativo no qual ela está fazendo a coleta".

Coleta Geracional Jovem

O coletor geracional jovem no CMS é denominado ParNew e na verdade usa o mesmo algorítimo básico do coletor Parallel Scavenge, um dos coletores paralelos que descrevi anteriormente.

Mesmo assim ele é um coletor diferente em termos da base de código hotspot da Parallel Scavenge, tanto porque precisa intercalar a sua execução com o resto da CMS, como também porque implementa uma API interna diferente para a Parallel Scavenge. Esta última faz presunções sobre quais coletores Tenured devem ser trabalhados por ela — especificamente ParOld e SerialOld. Tenha em mente que isso implica que o coletor geracional jovem também "para o mundo".

Coleta no espaço Tenured

Da mesma forma que o coletor ParOld o coletor CMS Tenured usa um algorítimo mark and sweep, no qual os objetos vivos são marcados e então os objetos mortos são apagados. Apagado é realmente um termo estranho quando se trata de gerenciamento de memória. O coletor não está na verdade deletando objetos no sentido de esvaziar a memória, ele está apenas devolvendo a memória associada com aquele objeto para o espaço no qual o sistema de gerenciamento possa alocá-la — os blocos livres. Não obstante seja denominado coletor de marcação e limpeza concorrente, nem todas as suas fases são executadas concorrentemente com o aplicativo: duas delas param o mundo e quatro são executadas concorrentemente.

Como é disparada a GC?

Em ParOld, a coleta de lixo é disparada quando se fica sem espaço na heap Tenured. Esta abordagem funciona porque ParOld simplesmente paralisa o aplicativo para efetuar a coleta. Para que o programa continue a rodar durante a coleta em Tenured, o coletor CMS precisa começar a coletar quando ainda há espaço de trabalho remanescente em Tenured.

Assim sendo, a CMS começa com base no montante de ocupação do espaço Tenured — a ideia é que a quantidade de espaço livre restante seja a janela de oportunidade para executar a GC. Isso é conhecido como fração de ocupação inicial e é descrita em termos de quanto a heap está ocupada. Assim, uma fração de 0.7 cria uma janela de 30% da heap para executar a GC CMS antes do seu esgotamento.

Fases

Uma vez que a GC seja disparada, o algorítimo CMS consiste de uma série de fases executadas em sequência:

  1. Marcação Inicial (Initial Mark) Paralisa todas as threads do aplicativo e marca como vivos todos os objetos diretamente alcançáveis a partir dos objetos-raiz. Esta fase para o mundo.

  2. Marcação Concorrente (Concurrent Mark) As threads do aplicativo são reiniciadas. Todos os objetos vivos são transitoriamente marcados enquanto alcançáveis pelas referências seguintes a partir dos objetos marcados na marcação inicial.

  3. Pré-limpeza Concorrente (Concurrent Preclean) Esta fase examina os objetos que tenham sido atualizados ou promovidos durante a marcação concorrente ou novos objetos que tenham sido alocados durante a mesma. O bit de marcação é atualizado para assinalar se o objeto está vivo ou morto. A fase pode ser executada repetidamente até que haja uma taxa específica de ocupação no Eden.

  4. Remarcação (Remark) Embora alguns objetos tenham sido atualizados durante a fase de pré-limpeza, ainda assim é preciso parar o mundo para processar objetos residuais. Esta fase faz uma revisão a partir das raízes. Ela também processa objetos de referência, tais como referências leves ou fracas. Esta fase para o mundo.

  5. Limpeza Concorrente (Concurrent Sweep) Examina a tabela de ponteiros de objetos ordinários (Ordinary Object Pointer - OOP), que referencia todos os objetos na heap, e localiza os objetos mortos. A memória alocada àqueles objetos é então readicionada à sua lista de blocos livres. Esta é a lista dos espaços nos quais um objeto pode ser alocado.

  6. Reinicialização Concorrente (Concurrent Reset) Reinicializa todas as estruturas de dados internas para que a CMS possa ser novamente executada no futuro.

Teoricamente, os objetos marcados durante a fase de pré-limpeza deveriam ser examinados na fase seguinte — remarcação — mas a fase de remarcação paralisa o mundo, de forma que a fase de pré-limpeza existe para tentar reduzir as pausas de remarcação, fazendo parte do trabalho de remarcação concorrentemente. Quando a CMS foi originalmente adicionada à HotSpot esta fase nem sequer existia. Ela foi adicionada na versão 1.5 de Java para manejar os cenários em que uma coleta e eliminação geracional jovem causa uma pausa e é imediatamente seguida de uma remarcação. Esta remarcação também causa uma pausa, que se combinam para gerar uma pausa ainda mais pronunciada. É por isso que as remarcações são disparadas por um limiar de ocupação do Eden — o objetivo é agendar a fase de remarcação para o meio do caminho entre as pausas geracionais jovens.

As fases de remarcação também fazem pausas, enquanto a de pré-limpeza não o faz. Isso significa que realizar pré-limpezas reduz a quantidade de tempo dispendido em pausas na GC.

Falhas do Modo Concorrente

Algumas vezes a CMS é incapaz de satisfazer as necessidades do aplicativo e uma GC Completa com paralisação do mundo precisa ser executada. Isso é chamado falha no modo concorrente, e normalmente resulta numa longa pausa. Uma falha no modo concorrente acontece quando não há espaço suficiente em Tenured para promover um objeto. Há duas causas para isso:

  • É promovido um objeto que é muito grande para caber em algum espaço contíguo de memória.
  • Não há espaço suficiente em Tenured para dar conta da taxa de objetos vivos sendo promovidos.

Isso pode ocorrer porque a coleta concorrente é incapaz de liberar espaço rápido o suficiente, devido às taxas de promoção de objetos, ou porque o uso continuado do coletor CMS tenha causado muita fragmentação da heap e não existem espaços individuais grandes o suficiente para receberem objetos promovidos. Para poder desfragmentar a heap Tenured adequadamente, é preciso executar uma GC completa.

Permgen

A CMS não coleta os espaços Permgen por padrão, exigindo que a flag XX:+CMSClassUnloadingEnabled seja habilitada para isso. Caso o espaço Permgen se esgote enquanto a CMS está sendo usada, sem que esta flag esteja habilitada, será disparada uma GC completa. Ademais, o espaço Permgen pode manter referências na heap normal através de estruturas como classloaders, significando que até que o Permgen seja coletado pode estar havendo vazamento de memória na heap regular. Em Java 7 as constantes String dos arquivos de classes também são alocadas na heap regular, ao invés de o serem no Permgen, o que por um lado reduz o uso desta, mas por outro soma-se ao conjunto de referências que estão entrando na heap regular provenientes do Permgen.

Lixo Flutuante (Floating Garbage)

No final de uma coleta CMS é possível que alguns objetos não tenham sido deletados — chama-se a isso Lixo Flutuante (Floating Garbage). Isso ocorre quando os objetos são de-referenciados após a marcação inicial. A pré-limpeza concorrente e a fase de remarcação asseguram-se de que todos os objetos vivos sejam marcados, examinando os objetos que tenham sido criados, modificados ou promovidos. Caso um objeto tenha sido de-referenciado entre a marcação inicial e a fase de remarcação, então será preciso uma pesquisa completa no grafo de objetos para encontrar todos os mortos. Isso é obviamente muito demorado, e a fase de remarcação deve ser breve uma vez que nela ocorrem pausas.

Isso não é necessariamente um problema para os usuários da CMS uma vez que a próxima execução do coletor CMS cuidará de limpar esse lixo remanescente.

Sumário

A marcação e limpeza concorrente (Concurrent Mark and Sweep) reduz os tempos de pausa do coletor paralelo executando parte do trabalho de GC ao mesmo tempo em que o aplicativo é executado. Ela não remove as pausas completamente, uma vez que parte de seu algorítimo precisa parar o aplicativo para que possa ser executado.


comments powered by Disqus