BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Java Garbage Collection Essencial

Java Garbage Collection Essencial

Favoritos

Serial, Parallel, Concurrent, CMS, G1, Young Gen, New Gen, Old Gen, Perm Gen, Eden, Tenured, Survivor Spaces, Safepoints e as centenas de flags de inicialização da JVM. Deixam tudo confuso quando se está tentando otimizar o garbage collector para obter a taxa de transferência e latência necessária para a aplicação Java? A documentação sobre a coleta de lixo parece um manual para aeronaves. Cada botão e seletor está explicado detalhadamente, mas em nenhum lugar é possível encontrar um guia sobre como voar. Este artigo vai tentar explicar as vantagens e desvantagens na escolha e ajuste dos algoritmos do garbage collector em uma determinada carga de trabalho.

O foco será nos coletores da Oracle Hotspot JVM e do OpenJDK que são de uso mais comum. No final, outras JVMs comerciais serão discutidas para mostrar alternativas.

Vantagens e desvantagens

O sábio povo continua nos dizendo: "não se consegue nada de graça". Quando queremos algo geralmente temos que abrir mão de algo em troca. Quando se trata da coleta de lixo trabalhamos com 3 principais variáveis que definem metas para os coletores:

  1. Taxa de transferência: O total de trabalho realizado por uma aplicação como uma proporção do tempo gasto no GC. Configurando a taxa de transferência para ‑XX:GCTimeRatio=99, 99 é o padrão que equivale a 1% do tempo do GC.
  2. Latência: O tempo gasto pelos sistemas em resposta a eventos que são afetados por pausas introduzidas através da coleta de lixo. Configure a latência para pausas do GC com -XX: MaxGCPauseMillis = <n>.
  3. Memória: A quantidade de memória que nossos sistemas usam para guardar o estado, que é frequentemente copiado e movido quando estão sendo geridos. O conjunto de objetos ativos mantidos pela aplicação em qualquer tempo é conhecido como Live Set. Tamanho máximo da heap (Maximum heap size) -Xmx<n> é um parâmetro de ajuste para configurar o heap size disponível para uma aplicação.

Observação: Muitas vezes o Hotspot não pode alcançar essas metas e vai silenciosamente continuar sem nenhum aviso, mesmo após ter falhado por uma grande margem.

Latência é a distribuição entre eventos. Pode ser aceitável ter um aumento médio de latência para reduzir o pior caso de latência, ou torná-los menos frequentes. Não devemos interpretar o termo "real-time" como menor latência possível; mas sim que se refere a ter uma latência determinística, independentemente da taxa de transferência.

Para cargas de trabalho de algumas aplicações, a taxa de transferência é a meta mais importante. Um exemplo seria um processamento em lote de longa duração, não importa se o processamento em lote é ocasionalmente interrompido por alguns segundos enquanto a coleta de lixo é executada, desde que o processamento em lote possa ser concluído logo.

Para praticamente todas as outras cargas de trabalho, desde aplicações interativas com humanos a sistemas de comércio financeiro, se um sistema não responder após alguns segundos ou milissegundos, isso pode significar um desastre. Em negócios financeiros frequentemente vale a pena negociar alguma taxa de transferência em troca de latência consistente. Podemos também ter aplicações que são limitadas pela quantidade de memória física disponível e ter que se manter presente, neste caso temos que melhorar o desempenho em ambos, latência e taxa de transferência.

A seguir algumas vantagens e desvantagens:

  • Em grande parte o custo da coleta de lixo, como um custo amortizado, pode ser reduzido fornecendo ao garbage collector algoritmos com mais memória.
  • Os piores casos observados de pausas induzidas por latência devido à coleta de lixo podem ser reduzidos limitando o live set e mantendo a heap pequena.
  • A frequência com que as pausas ocorrem pode ser reduzida pelo gerenciamento da heap e o tamanho das gerações, e controlando a taxa de alocação de objetos.
  • A grande frequência de pausas pode ser reduzida pela execução do GC concorrentemente com a aplicação, às vezes à custa da taxa de transferência.

Ciclo de vida dos Objetos

Os algoritmos de coleta de lixo são frequentemente otimizados na expectativa de que a maioria dos objetos viva por um curto período de tempo, enquanto que relativamente poucos vivem por muito tempo. Na maioria das aplicações, os objetos que vivem por um período significativo de tempo tendem a constituir uma porcentagem muito pequena dos objetos alocados ao longo do tempo. Na teoria, esse comportamento observado na coleta de lixo é conhecido como "mortalidade infantil" ou "hipótese geracional fraca". Por exemplo, loops em Iterators são em sua maioria de curta duração enquanto que Strings são efetivamente imortais.

A experiência tem demonstrado que os garbage collectors geracionais normalmente podem suportar uma taxa de transferência maior que os collectors não geracionais, portanto são quase sempre usados em servidores JVMs. Pela separação das gerações de objetos, sabemos que a região na qual os novos objetos são alocados é provavelmente muito escassa para objetos vivos. Portanto, um collector que varre procurando por objetos vivos nessa região e os copia para outra região de objetos mais velhos pode ser muito eficiente. Os coletores de lixo do Hotspot gravam a idade de um objeto de acordo com o número de ciclos do GC que sobreviveu.

Observação: Se uma aplicação sempre gera um monte de objetos que vivem por um tempo bastante longo, então é de se esperar que essa aplicação gaste uma porção significativa deste tempo com a coleta de lixo, assim como tenha que se gastar um tempo significativo ajustando o coletor de lixo do Hotspot. Isto é devido à redução da eficiência do GC que acontece quando o "filtro" geracional é menos efetivo, e ao custo resultante de coletar gerações vivas há mais tempo com uma maior frequência. Gerações mais velhas são menos escassas e, como resultado, a eficiência dos algoritmos de coleta das gerações mais velhas tendem a ser bem ruim. Os Garbage Collectors geracionais tendem a operar em dois ciclos de coletas distintos: as coletas Menores (Minor collections), em que os objetos de curta duração são coletados, e as menos frequentes coletas Maiores (Major collections), em que as regiões mais velhas são coletadas.

Eventos do tipo Para-O-Mundo (Stop-The-World)

As pausas sofridas pelas aplicações durante a coleta de lixo são devidas aos, como são conhecidos, "eventos do tipo stop-the-world". Por razões práticas de engenharia, para que o garbage collector opere, é necessário parar a aplicação em execução periodicamente para que a memória possa ser gerenciada. Dependendo dos algoritmos, diferentes coletores dispararão o evento stop-the-world em específicos pontos da execução, variando o tempo da parada. Para fazer uma aplicação parar totalmente é necessário pausar todas as threads em execução. Os garbage collectors fazem isso avisando as threads para parar quando elas estiverem em um "ponto seguro", que é o ponto durante a execução do programa no qual é conhecido por todos os GC e todos os objetos na heap estão consistentes. Dependendo do que a thread estiver executando, pode demorar algum tempo até ela alcançar um ponto seguro. Pontos seguros são normalmente estabelecidos nos retornos de método e nos finais dos loops, mas podem ser otimizados para alguns pontos diferentes, tornando-os dinamicamente mais raros. Por exemplo, se uma Thread está copiando um grande array, clonando um objeto grande, ou executando um loop, pode demorar muitos milissegundos antes de um ponto seguro ser atingido. O tempo para atingir um ponto seguro é uma preocupação importante em aplicações com baixa latência. Este tempo pode ser visualizado habilitando a flag ‑XX:+PrintGCApplicationStoppedTime junto com as outras flags do GC.

Observação: Para aplicações com um número grande de threads em execução, quando um evento stop-the world ocorre, o sistema sofrerá uma pressão enorme assim que as threads estiverem liberadas dos pontos seguros. Por isso que algoritmos meno sependentes de eventos stop-the-world são potencialmente mais eficientes.

Organização da Heap no Hotspot

Para entender como os diferentes coletores funcionam é melhor explorar como a Heap é organizada para suportar os coletores geracionais.

O Eden é a região na qual a maioria dos objetos são inicialmente alocados. Os objetos que sobreviveram a uma coleta no Eden são temporariamente armazenados nos survivor spaces. O uso do Survivor será descrito quando as coletas menores forem discutidas. O Eden e o Survivor são espaços conhecidos como geração "young" e "new", respectivamente.

Os objetos que vivem durante um tempo longo o suficiente são eventualmente promovidos para a tenured space.

A perm gen é o lugar em que a Runtime (JVM) armazena os objetos "conhecidos" por serem efetivamente imortais, tais como Classes e Strings estáticas. Infelizmente o uso comum do carregamento de classes de forma contínua em muitas aplicações motiva a equivocada suposição de que por trás da perm gen as classes são imortais. No Java 7 as Strings internas foram movidas da perm gen para a tenured, e a partir do Java 8 a perm gen não existirá mais, e não será abordada neste artigo. A maioria dos coletores comerciais não usa uma perm gen separada e tende a tratar todos os objetos de longa duração na tenured.

Observação: As áreas virtuais permitem que os coletores ajustem o tamanho das regiões para cumprir as metas de taxa de transferência e latência. Os coletores mantêm estatísticas para cada fase da coleta e ajustam o tamanho da região na tentativa de cumprir as metas.

Alocação de Objetos

Para evitar disputas, cada thread é atribuída a um buffer de alocação de thread local (Thread Local Allocation Buffer - TLAB) no qual os objetos são alocados. O uso de TLABs permite que a alocação de objetos possa escalar de acordo com número de threads, evitando disputas de um único recurso na memória. A alocação de objetos via TLAB é uma operação muito barata, a TLAB simplesmente aponta o ponteiro para o tamanho do objeto que leva cerca de 10 instruções na maioria das plataformas. A alocação da pilha de memória para o Java é ainda mais barato do que usar o malloc do runtime existente no C.

Observação: Visto que a alocação individual de objetos é muito barata, a taxa à qual coletas menores devem ocorrer é diretamente proporcional à taxa de alocação do objeto.

Quando uma TLAB está exausta uma thread simplesmente socilita uma nova para o Eden. Quando o Eden está cheio uma coleta menor é iniciada.

Pode acontecer de não ser possível alocar grandes objetos (-XX:PretenureSizeThreshold=n) na young gen e assim eles terão de ser alocados na old gen, por exemplo um grande array. Se o limite configurado é menor que o tamanho do TLAB, então os objetos que couberem no TLAB não serão criados na old gen. O novo coletor G1 manipula grandes objetos diferentemente e o mesmo será discutido mais adiante.

Coletas menores

Uma coleta menor é disparada quando o Eden fica cheio. Todo os objetos vivos na new generation são copiados para o survivor space ou o tenured space, conforme a necessidade.

A cópia para o tenured space é conhecida como uma promoção ou amadurecimento. Promoções ocorrem para objetos que são suficientemente velhos (-- XX:maxTenuringThreshold), ou quando o survivor space estoura.

Objetos vivos são objetos que podem ser acessados pela aplicação, quaisquer outros objetos que não possam ser acessados podem portanto ser considerados mortos. Em uma coleta menor, a cópia dos objetos vivos é feita primeiramente pelo que é conhecido como GC Roots, e de forma iterativa copia qualquer objeto acessível para o survivor space. GC Roots normalmente incluem referências da aplicação e campos estáticos internos da JVM, e pilhas de threads, tudo o que efetivamente aponta para gráficos de objetos acessíveis da aplicação.

Em coletas geracionais, os GC Roots para os gráficos de objetos alcançáveis da new gen também incluem qualquer referência da old gen para a new gen. Estas referências devem também ser processadas para garantir que todos objetos alcançáveis na new gen sobrevivem a coleta menor. Identificar essas referências entre gerações é possível através do uso de um "cartão de mesa".

O cartão de mesa do Hotspot é um array de bytes no qual cada byte é usado para marcar a potencial existência de referências entre gerações em uma região correspondente a 512 bytes da old gen. As referências são armazenadas na heap, a "barreira de armazenamento" de código marcará os cartões para indicar que uma potencial referência da old gen para a new gen possa existir na região 512 byte associada. No momento da coleta, o cartão de mesa é usado para procurar por referências entre gerações, que efetivamente representam GC Roots adicionais para a new gen. Entretanto um custo fixo significativo nas coletas menores é diretamente proporcional ao tamanho da old gen.

Existem duas survivor spaces na new gen da Hotspot, que se alternam em suas regras em seus "espaço destino" e "espaço origem". No início de uma coleta menor, o "espaço destino" survivor space é sempre vazio, e atua como uma área de cópia para a coleta menor. O survivor space da coleta menor anterior é parte do "espaço origem", que também inclui o Eden, local que os objetos vivos que precisam ser copiados podem ser encontrados.

O custo de um GC de coleta menor é geralmente dominado pelo custo da cópia de objetos para os survivor spaces e tenured spaces. Objetos que não sobrevivem a coleta menor estão efetivamente livres para serem tratados. O trabalho realizado durante a coleta menor é diretamente proporcional ao número de objetos vivos encontrados, e não ao tamanho da new gen. O tempo total gasto executando a coleta menor pode ser praticamente reduzida a metade cada vez que o tamanho do Eden é dobrado. A memória portanto pode ser trocada por taxa de transferência. A duplicação do tamanho Eden resultará no aumento do tempo de coleta por ciclo, mas isso é relativamente pequeno se tanto o número de objetos a serem promovidos como o tamanho de geração mais velha é constante.

Observação: Na Hotspot as coletas menores são eventos stop-the-world. Isso está rapidamente se tornando uma questão importante conforme nossas heaps ficam maiores com mais objetos vivos. Já vemos a necessidade de coletas concorrentes da young gen para atingirmos as metas de pausa de tempo.

Coletas maiores

Coletas maiores ocorrem na old gen para que os objetos possam ser promovidos da young gen. Na maioria das aplicações, na grande maioria os estados dos programas terminam na old gen. Há uma grande variedade de algoritmos existentes para a old gen. Alguns vão compactar todo o espaço quando encher, enquanto outros vão coletar concorrentemente com a aplicação para tentar impedi-lo de encher.

O coletor da old gen vai tentar adivinhar quando é necessário coletar para evitar um fracasso na promoção da young gen. Os coletores monitoram um limite de preenchimento para a old gen e começam a coletar quando este limite é ultrapassado. Se este limite não é suficiente para atender as necessidades de promoção, então um "FullGC" é acionado. O FullGC envolve a promoção de todos os objetos vivos da new gen seguidas por uma coleta e compactação da old gen. A falha de promoção é uma operação muito cara e os objetos promovidos a partir deste ciclo deve ser desfeitos para que o evento FullGC possa ocorrer.

Observação: Para evitar a falha na promoção será necessário ajustar o preenchimento que a old gen permite para acomodar promoções (‑XX:PromotedPadding=<n>).

Observação: Quando a Heap precisa aumentar um FullGC é acionado. Esse redimensionamento da Heap por FullGCs podem ser evitado ajustando -Xms e -Xmx para o mesmo valor.

Além do FullFC, uma compactação da old gen é provávelmente o maior evento stop-the-world que uma aplicação vai experimentar. O tempo para esta compactação tende a crescer linearmente com o número de objetos vivos no tenured space.

A taxa à qual a tenured space se enche por vezes pode ser reduzido incrementando o tamanho do survivor space e a idade dos objetos antes de serem promovidos para a tenured gen. Entretanto, aumentando o tamanho do survivor space e a idade dos objetos nas coletas menores (-XX:MaxTenuringThreshold) antes da promoção também pode aumentar os custos e tempos de pausa nas coletas menores devido ao aumento do custo de cópia entre os survivor space em coleções menores.

Coleta em série

A coleta em série (-XX:+UseSerialGC) é o coletor mais simples e uma boa opção para sistemas com um único processador. Ele também tem o menor tamanho de qualquer coletor. Ele usa uma única thread para a coleta menor e maior. Os objetos são alocados no tenured space usando um simples algoritmo de colisão de ponteiros. Coletas maiores são acionadas quando o tenured space está cheio.

Coletor paralelo

O coletor Paralelo vem em duas formas. O Coletor Paralelo (‑XX:+UseParallelGC) que usa múltiplas threads para executar coleções menores da young gen e uma única thread para coletas maiores na old gen. O Coletor Paralelo Antigo (‑XX:+UseParallelOldGC), o padrão desde o Java 7u4, usa múltiplas threads para coletas menores e múltiplas threads para coletas maiores. Os objetos são alocados no tenured space usando um simples algoritmo de colisão de ponteiros. Coletas maiores são acionadas quando o espaço tenured está cheio.

Em sistemas com muitos processadores o Coletor Paralelo Antigo vai dar melhor vazão que qualquer coletor. Ele não impacta em uma aplicação em execução até a coleta acontecer, e então vai coletar em paralelo usando múltiplas threads usando um algoritmo mais eficiente. Isso faz o Coletor Paralelo Antigo muito eficiente para aplicação em batch.

O custo de coletar as gerações old é afetado pelo número de objetos mantendo uma maior extensão do que o tamanho da heap. Portanto a eficiência do Coletor Paralelo Antigo pode ser aumentada para alcançar uma maior taxa de transferência, fornecendo mais memória e aceitando maiores, mas em número menor, pausas para coleta.

Espere coletas menores mais rápidas com este coletor porque a promoção para o tenured space é um simples operação de cópia e acerto de ponteiros.

Para servidores de aplicações o Coletor Paralelo Old deve ser a porta de entrada. Entretanto se as pausas para coletas maiores são maiores que a aplicação pode tolerar então será preciso considerar empregar um coletor concorrente que colete os objetos na tenured concorrentemente enquanto a aplicação está em execução.

Observação: Espere pausas em ordem de 1 a 5 segundos por GB de informação viva em máquinas modernas enquanto a old gen é compactada.

Coletor Concurrent Mark Sweep (CMS)

O coletor Concurrent Mark Sweep (CMS) (-XX:+UseConcMarkSweepGC) é executado na old gen coletando objetos maduros que não são mais acessíveis durante a coleta maior. Ele é executado concorrentemente na aplicação com o objetivo de manter espaço livre o suficiente na old gen, então falhas de promoção na young gen não ocorrem.

A falha de promoção irá desencadear um FullGC. O CMS segue um processo de várias etapas:

  1. Marcação inicial <stop-the-world>: Procura GC Roots;
  2. Marcação concorrente: Marca todos os objetos acessíveis pelo GC Roots;
  3. Pré-limpeza concorrente: Verifica referências de objetos que foram atualizadas e objetos que foram promovidos durante a fase de remarcação concorrente;
  4. Remarcação <stop-the-world>: Captura referências de objetos que foram atualizados desde a fase de pré-limpeza;
  5. Varredura concorrente: Atualiza as listas livre com a recuperação da memória ocupada por objetos mortos;
  6. Reinicio concorrente: Reinicia a estrutura de dados para a próxima execução.

Assim que os objetos maduros se tornam inacessíveis, o espaço é recuperado pelo CMS e colocado em listas livres. Quando a promoção acontece, deve ser pesquisado um local de tamanho adequado para as listas livres para o objeto promovido. Isso aumenta o custo da promoção e assim aumenta o custo da coleta menor comparado ao Coletor Paralelo.

Observação: O CMS não é um coletor de compactação, o que ao longo do tempo pode resultar na fragmentação da old gen. A promoção do objeto pode falhar, pois um objeto grande pode não caber nos locais disponíveis na old gen. Quando isso acontece uma mensagem "promoção falha" é registrada e um FullGC é acionado para compactar os objetos maduros vivos. Para tais compactações orientadas ao FullGCs, espere pausas piores que das coletas maiores usando o Coletor Paralelo Antigo porque CMS usa uma simples thread para a compactação.

O CMS é na maior parte concorrente com a aplicação, que tem um número de implicações. Primeiro, o tempo da CPU é consumido pelo coletor, deste modo reduzindo a CPU disponível para a aplicação. O total de tempo requerido pelo CMS aumenta linearmente com o montante de objetos promovidos para o tenured space. Segundo, para algumas fases concorrentes do ciclo do GC, todas as threads tem que ser trazidas para um ponto a salvo para o GC Roots marcar e realizar uma remarcação paralela para verificar se há mutações.

Observação: Se uma aplicação percebe mutações significantes nos objetos maduros, então uma fase de remarcação pode ser significante, nos extremos pode levar mais tempo do que uma compactação completa com o Coletor Paralelo Antigo.

O CMS faz do FullGC um evento menos frequente à custa da redução da taxa de transferência, coletas menores são mais caras, e mais marcantes. A redução na taxa de transferência pode ser qualquer coisa entre 10% a 40% em relação ao Coletor Paralelo, dependendo da taxa de promoção. O CMS também exige 20% mais memória para acomodar estruturas de dados adicionais e "lixo flutuante" que pode ser perdido durante a marcação simultânea que será transferida para o próximo ciclo.

Altas taxas de promoção e fragmentação resultante podem às vezes ser reduzidas pelo aumento do tamanho de ambos a young gen e old gen.

Observação: O CMS pode sofrer "falhas de modo concorrente", que pode ser visto nos logs, quando deixa de coletar a uma taxa suficiente para manter-se com a promoção. Isso pode ser causado quando a coleta começa tarde, que podem ser definidos por meio de ajuste. Mas isso também pode acontecer quando a taxa de coleta não consegue acompanhar a alta taxa de promoção ou a alta mutação de objetos de algumas aplicações. Se o taxa de promoção ou mutação da aplicação é tão alta, então sua aplicação pode necessitar de algumas mudanças para reduzir a pressão da promoção. Adicionando mais memória para um sistema deste tipo, por vezes, pode piorar a situação, porque o CMS teria mais memória para examinar.

Coletor Garbage First (G1)

O Garbage First (G1) (-XX:+UseG1GC) é um novo coletor introduzido no Java 6 e agora oficialmente suportado no Java 7. É um algoritmo parcialmente concorrente que também tenta compactar o tenured space em pequenas e incrementais pausas stop-the-world para tentar minimizar os eventos FullGC que aflige o CMS por causa da fragmentação. G1 é um coletor geracional que organiza a heap diferentemente dos outros coletores dividindo-o em regiões de tamanho fixo de efeito variável, ao invés de regiões contíguas com o mesmo objetivo.

O G1 adota a abordagem de concorrentemente marcar regiões para rastrear referências entre regiões, e concentra a coleta nas regiões com mais espaço livre. Estas regiões são então coletadas em uma pausa stop-the-world descarregando os objetos vivos para uma região vazia, assim compactando no processo. Objetos maiores que 50% da região são alocados em uma região monstruosa, que é um múltiplo do tamanho da região. A alocação e coleta de objetos monstruosos podem ser muito caros para o G1, e até agora teve pouco ou nenhum esforço de otimização aplicada.

O desafio com qualquer coletor de compactação não é o de mover objetos mas o de atualizar as referências destes objetos. Se um objeto é referenciado em várias regiões, então atualizar estas referências podem demorar significativamente mais do que mover o objeto. O G1 rastreia quais objetos em uma região tem referências de outras regiões via "Remembered Sets" (Conjuntos lembrados). Se um Remembered Set tornar-se grande, então o G1 pode desacelerar significativamente. Quando liberando objetos de uma região para outra, o tamanho dos eventos stop-the-world associados tendem a ser proporcionais ao número de regiões com referências que precisam ser escaneadas e potencialmente corrigidas.

Manter os Remembered Sets aumenta o custo das coletas menores resultando pausas maiores do que visto com o Coletor Paralelo Antigo ou o CMS para coleções menores.

O G1 é configurado baseado na latência -XX:MaxGCPauseMillis=<n>, valor padrão = 200ms. A meta irá influenciar a quantidade de trabalho realizado em cada ciclo somente na base nos melhores esforços. Estabelecer metas em dezenas de milissegundos é mais fútil, e da forma como foi escrito, tentar alvejar dezenas de milissegundos não foi o foco do G1.

O G1 é em geral um bom coletor para grandes pilhas que tem a tendência de se tornar fragmentado quando uma aplicação pode tolerar pausas entre 0.5 e 1.0 segundo para compactações incrementais. O G1 tende a reduzir a frequência dos piores casos de pausa visto no CMS devido a fragmentação ao custo das coletas menores e compactação incremental da old gen. Mais pausas acabam sendo obrigatórias para o desenvolvimento regional ao invés de compactações de pilhas cheias.

Como o CMS, o G1 pode deixar de manter-se com as taxas de promoção, e voltará para um FullGC stop-the-world. Assim como o CMS tem "concorrente modo de falha", G1 pode sofrer uma falha de evacuação, visto nos logs como "estouro de espaço". Isso ocorre quando não existem regiões livres na qual os objetos possam ser evacuados, que é similar a uma falha de promoção. Se isso ocorrer, tente usar uma grande pilha e mais threads de marcação, mas em alguns casos mudanças na aplicação são necessárias para reduzir os custos de alocação.

Um problema desafiador para o G1 é lidar com objetos populares e regiões. Compactação incremental stop-the-world funciona bem quando regiões tem objetos vivos que não são fortemente referenciados em outras regiões. Se um objeto ou região é popular então o Remembered Set será grande e o G1 evitará coletar estes objetos. Eventualmente ele pode não ter escolha, que resultará em muitas pausas frequentes de média duração até que a heap seja compactada.

Coletores Concorrentes Alternativos

CMS e G1 são frequentemente chamados de coletores concorrentes. Quando olhamos o trabalho total realizado é claro que a young gen, promoção e até mesmo o trabalho da old gen não é concorrente ao todo. O CMS é mais concorrente para a old gen; O G1 é muito mais do que um coletor incremental do tipo stop-the-world. Ambos CMS e G1 tem significantes e regulares ocorrências dos eventos stop-the-world, e nos piores cenários que frequentemente tornam-nos impróprios para rigorosas aplicações de baixa latência, como uma negociação financeira or interfaces reativas ao usuário.

Coletores alternativos como Oracle JRockit Real Time, IBM Websphere Real Time e Azul Zing estão disponíveis. Os coletores JRockit e Websphere levam vantagem na latência na maioria dos casos sobre o CMS e G1, mas frequentemente veem limitações na faixa de transferência e continuam sofrendo significantes eventos stop-the-world. Zinf é o único coletor Java conhecido por este autor que pode ser verdadeiramente concorrente para coleta e compactação enquanto mantém uma alta taxa de transferência para todas as gerações. Zing tem algumas frações de milisengundos em eventos stop-the-world, mas há trocas de fases no ciclo de coleta que não estão relacionados ao tamanho do conjunto vivo.

JRockit RT pode alcançar tempos de pausa típicos em dezenas de milisegundos para altas taxas de alocação contidas no tamanho da pilha, mas as vezes tem que deixar para traz a pausa para a compactação completa. Websphere RT pode alcançar pausas de poucos milisegundos através de taxa de alocação restrita e tamanho de conjuntos vivos. O Zing pode alcançar pausas de frações de milisegundos com altas taxas de alocação por ser concorrente por todas fases, incluindo durante as coletas menores. O Zing tem condições de manter esse comportamento consistente graças ao tamanho da heap, permitindo que o usuário aplique grandes tamanhos de heap conforme preciso, acompanhando a taxa de tranferência do aplicativo ou a necessidade dos estados dos objetos, sem medo de maiores tempos de pausa.

Para todos os coletores concorrentes focados na latência é necessário dar um pouco de taxa de transferência e ganho de rastro. Dependendo da eficiência do coletor concorrente pode-se dar um pouco de taxa de transferência, mas está sempre adicionando rastro significativo. Se for realmente concorrente, com poucos eventos stop-the-world, então mais núcleos de CPU serão necessários para habilitar a concorrência e manter a vazão.

Observação: Todos coletores concorrentes tendem a funcionar mais eficientemente quando há espaço suficiente para a alocação. Como regra de ponto de partida tente reservar uma heap com pelo menos duas ou três vezes o tamanho dos conjuntos vivos, para a operação ser eficiente. Entretanto, o espaço necessário para manter as operações concorrentes aumentam com a taxa de transferência da aplicação, e está associada a alocação e taxas de promoção. Então para aplicações de maior vazão, uma heap maior para um conjunto vivo proporcional pode ser justificado. Dados os enormes espaços de memória disponíveis para os sistemas de hoje o uso da memória raramente é um problema no lado do servidor.

Monitorando e Ajustando a Coleta de Lixo

Para entender como a aplicação e o garbage collector estão se comportando, inicie a JVM com as seguintes configurações:

-verbose:gc
-Xloggc:<filename>
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime

Então carregue os logs dentro de uma ferramenta como Chewiebug para realizar as analises.

Para ver a natureza dinâmica do GC, execute o JVisualVM e instale o plugin Visual GC que permitirá a visualização do GC em ação, como por exemplo na aplicação a seguir.

Para entender o que o GC da aplicação precisa é necessário carregar testes representativos que possam ser rodados repetidamente. À medida que se familiariza com a forma como cada coletor trabalha, então execute os teste de carga com diferentes configurações até chegar na taxa de transferência e latência desejadas. É importante medir a latência da perspectiva do usuário final. Isso pode ser alcançado capturando o tempo de resposta de cada requisição do teste em um historograma, e é possível ler mais sobre isso aqui. Se houver picos de latência que estão fora do range aceitável, então tente relacioná-las com os logs do GC para determinar se o problema é o GC. É possível que outras questões possam causar os picos de latência. Outra ferramenta útil a considerar é o jHiccup que pode ser usado para acompanhar as pausas dentro da JVM e através do sistema como um todo.

Se os picos de latência são devidos ao GC, então invista em ajustar o CMS ou G1 para ver se suas métricas de latência podem ser cumpridas. As vezes isso não é possível devido a alta alocação e taxas de promoção combinadas com os requisitos de baixa latência. Os ajustes do GC podem se tornar um exercício altamente necessário e que muitas vezes requer mudanças de aplicativos para reduzir as taxas de alocação ou ciclo de vida dos objetos. Se for o caso, então avalie a economia de tempo e recursos gastos no ajustes do GC e mudanças na aplicação com a compra de um dos concorrentes comerciais de compactação de JVMs como JRockit Real Time Azul ou Zing.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT