Java Virtual Machine - GCs de geração

A maioria das JVMs divide o heap em três gerações - the young generation (YG), the old generation (OG) and permanent generation (also called tenured generation). Quais são as razões por trás desse pensamento?

Estudos empíricos mostraram que a maioria dos objetos criados têm uma vida útil muito curta -

Fonte

https://www.oracle.com

Como você pode ver, à medida que mais e mais objetos são alocados com o tempo, o número de bytes sobreviventes se torna menor (em geral). Objetos Java têm alta taxa de mortalidade.

Veremos um exemplo simples. A classe String em Java é imutável. Isso significa que toda vez que você precisa alterar o conteúdo de um objeto String, deve criar um novo objeto. Vamos supor que você faça alterações na string 1000 vezes em um loop, conforme mostrado no código a seguir -

String str = “G11 GC”;

for(int i = 0 ; i < 1000; i++) {
   str = str + String.valueOf(i);
}

Em cada loop, criamos um novo objeto string, e a string criada durante a iteração anterior torna-se inútil (ou seja, não é referenciada por nenhuma referência). O tempo de vida desse objeto foi apenas uma iteração - eles serão coletados pelo GC em nenhum momento. Esses objetos de vida curta são mantidos na área de geração jovem do heap. O processo de coleta de objetos da geração mais jovem é chamado de coleta de lixo secundária e sempre causa uma pausa para 'parar o mundo'.

À medida que a geração jovem vai ficando cheia, o GC faz uma pequena coleta de lixo. Objetos mortos são descartados e objetos vivos são movidos para a geração anterior. Os threads do aplicativo param durante este processo.

Aqui, podemos ver as vantagens que esse design de geração oferece. A geração jovem é apenas uma pequena parte da pilha e fica cheia rapidamente. Mas o processamento leva muito menos tempo do que o tempo necessário para processar todo o heap. Portanto, as pausas 'pare o mundo' neste caso são muito mais curtas, embora mais frequentes. Devemos sempre ter como objetivo pausas mais curtas em vez de pausas mais longas, embora possam ser mais frequentes. Discutiremos isso em detalhes em seções posteriores deste tutorial.

A geração jovem está dividida em dois espaços - eden and survivor space. Os objetos que sobreviveram durante a coleta do éden são movidos para o espaço do sobrevivente, e aqueles que sobrevivem no espaço do sobrevivente são movidos para a geração anterior. A geração jovem é compactada enquanto é coletada.

Conforme os objetos são movidos para a geração anterior, eles eventualmente se enchem e precisam ser coletados e compactados. Diferentes algoritmos têm diferentes abordagens para isso. Alguns deles param os encadeamentos do aplicativo (o que leva a uma longa pausa 'stop-the-world', já que a geração antiga é muito grande em comparação com a geração mais jovem), enquanto alguns deles o fazem simultaneamente, enquanto os encadeamentos do aplicativo continuam em execução. Este processo é denominado GC completo. Dois desses colecionadores sãoCMS and G1.

Vamos agora analisar esses algoritmos em detalhes.

Serial GC

é o GC padrão em máquinas de classe cliente (máquinas de processador único ou JVM 32b, Windows). Normalmente, os GCs são altamente multithread, mas o GC serial não. Ele tem um único thread para processar o heap e interromperá os threads do aplicativo sempre que estiver fazendo um GC secundário ou um GC principal. Podemos comandar a JVM para usar este GC, especificando o sinalizador:-XX:+UseSerialGC. Se quisermos que ele use algum algoritmo diferente, especifique o nome do algoritmo. Observe que a geração anterior é totalmente compactada durante um GC principal.

Taxa de transferência GC

Este GC é padrão em JVMs de 64b e máquinas multi-CPU. Ao contrário do GC serial, ele usa vários threads para processar a nova e a velha geração. Por causa disso, o GC também é chamado deparallel collector. Podemos comandar nossa JVM para usar este coletor usando o sinalizador:-XX:+UseParallelOldGC ou -XX:+UseParallelGC(para JDK 8 em diante). Os encadeamentos do aplicativo são interrompidos enquanto ele faz uma coleta de lixo principal ou secundária. Como o coletor serial, ele compacta totalmente a geração jovem durante um grande GC.

O throughput GC coleta o YG e o OG. Quando o eden está cheio, o coletor ejeta objetos vivos dele para o OG ou um dos espaços sobreviventes (SS0 e SS1 no diagrama abaixo). Os objetos mortos são descartados para liberar o espaço que ocupavam.

Antes do GC de YG

Depois de GC de YG

Durante um GC completo, o coletor de rendimento esvazia todo o YG, SS0 e SS1. Após a operação, o OG contém apenas objetos vivos. Devemos observar que ambos os coletores acima param os threads do aplicativo durante o processamento do heap. Isso significa longas pausas de 'parar o mundo' durante um grande GC. Os próximos dois algoritmos visam eliminá-los, ao custo de mais recursos de hardware -

Coletor CMS

Significa 'varredura de marcação simultânea'. Sua função é usar algumas threads de fundo para varrer a geração anterior periodicamente e se livrar de objetos mortos. Mas durante um GC menor, os threads do aplicativo são interrompidos. No entanto, as pausas são bastante pequenas. Isso torna o CMS um coletor de baixa pausa.

Este coletor precisa de tempo de CPU adicional para varrer o heap enquanto executa os threads do aplicativo. Além disso, os threads de fundo apenas coletam o heap e não executam nenhuma compactação. Eles podem fazer com que a pilha se torne fragmentada. À medida que isso continua, após um certo ponto de tempo, o CMS irá parar todos os threads do aplicativo e compactar o heap usando um único thread. Use os seguintes argumentos JVM para informar a JVM para usar o coletor CMS -

“XX:+UseConcMarkSweepGC -XX:+UseParNewGC” como argumentos JVM para instruí-lo a usar o coletor CMS.

Antes do GC

Depois de GC

Observe que a coleta está sendo feita simultaneamente.

G1 GC

Esse algoritmo funciona dividindo o heap em várias regiões. Como o coletor CMS, ele interrompe os encadeamentos do aplicativo enquanto faz um GC menor e usa encadeamentos em segundo plano para processar a geração anterior, enquanto mantém os encadeamentos do aplicativo em andamento. Como dividiu a geração anterior em regiões, ele continua compactando-as enquanto move objetos de uma região para outra. Portanto, a fragmentação é mínima. Você pode usar a bandeira:XX:+UseG1GCpara dizer ao seu JVM para usar este algoritmo. Como o CMS, ele também precisa de mais tempo de CPU para processar o heap e executar os threads do aplicativo simultaneamente.

Este algoritmo foi projetado para processar heaps maiores (> 4G), que são divididos em várias regiões diferentes. Algumas dessas regiões compreendem a geração jovem e o restante compreende a geração mais velha. O YG é limpo usando tradicionalmente - todos os encadeamentos do aplicativo são interrompidos e todos os objetos que ainda estão vivos para a geração anterior ou o espaço sobrevivente.

Observe que todos os algoritmos de GC dividiram o heap em YG e OG e usam um STWP para limpar o YG. Esse processo geralmente é muito rápido.