O mecanismo de coleta de lixo da plataforma Java melhorou significativamente a eficiência do desenvolvedor, mas um coletor de lixo mal implementado pode consumir muitos recursos do aplicativo. Na terceira parte da série de otimização de desempenho de máquinas virtuais Java, Eva Andreasson apresenta aos iniciantes em Java o modelo de memória e o mecanismo de coleta de lixo da plataforma Java. Ela explica por que a fragmentação (e não a coleta de lixo) é o principal problema no desempenho de aplicativos Java e por que a coleta de lixo geracional e a compactação são atualmente as principais (mas não as mais inovadoras) formas de lidar com a fragmentação de aplicativos Java.
O objetivo da coleta de lixo (GC) é liberar a memória ocupada por objetos Java que não são mais referenciados por nenhum objeto ativo. É a parte central do mecanismo de gerenciamento dinâmico de memória da máquina virtual Java. Durante um ciclo típico de coleta de lixo, todos os objetos que ainda são referenciados (e, portanto, acessíveis) são retidos, enquanto aqueles que não são mais referenciados são liberados e o espaço que ocupam é recuperado para alocação a novos objetos.
Para entender o mecanismo de coleta de lixo e vários algoritmos de coleta de lixo, primeiro você precisa saber algo sobre o modelo de memória da plataforma Java.
Coleta de lixo e modelo de memória da plataforma Java
Quando você inicia um programa Java a partir da linha de comando e especifica o parâmetro de inicialização -Xmx (por exemplo: java -Xmx:2g MyApp), a memória do tamanho especificado é alocada para o processo Java, que é o chamado heap Java . Este espaço de endereço de memória dedicado é usado para armazenar objetos criados por programas Java (e às vezes pela JVM). À medida que o aplicativo é executado e aloca memória continuamente para novos objetos, o heap Java (ou seja, o espaço de endereço de memória dedicado) será preenchido lentamente.
Eventualmente, o heap Java ficará cheio, o que significa que o encadeamento de alocação de memória não consegue encontrar um espaço contíguo grande o suficiente para alocar memória para o novo objeto. Nesse momento, a JVM decide notificar o coletor de lixo e iniciar a coleta de lixo. A coleta de lixo também pode ser acionada chamando System.gc() no programa, mas usar System.gc() não garante que a coleta de lixo será executada. Antes de qualquer coleta de lixo, o mecanismo de coleta de lixo determinará primeiro se é seguro realizar a coleta de lixo. Quando todos os threads ativos do aplicativo estiverem em um ponto seguro, uma coleta de lixo poderá ser iniciada. Por exemplo, a coleta de lixo não pode ser executada quando a memória está sendo alocada para um objeto, ou a coleta de lixo não pode ser executada enquanto as instruções da CPU estão sendo otimizadas, porque o contexto provavelmente será perdido e o resultado final será incorreto.
O coletor de lixo não pode recuperar nenhum objeto com referências ativas, o que quebraria a especificação da Java Virtual Machine. Não há necessidade de reciclar objetos mortos imediatamente, porque os objetos mortos serão eventualmente reciclados pela coleta de lixo subsequente. Embora existam muitas maneiras de implementar a coleta de lixo, os dois pontos acima são os mesmos para todas as implementações de coleta de lixo. O verdadeiro desafio da coleta de lixo é como identificar se um objeto está ativo e como recuperar a memória sem afetar ao máximo o aplicativo. Portanto, o coletor de lixo tem os dois objetivos a seguir:
1. Libere rapidamente memória não referenciada para atender às necessidades de alocação de memória do aplicativo e evitar estouro de memória.
2. Minimize o impacto no desempenho do aplicativo em execução (latência e taxa de transferência) ao recuperar memória.
Dois tipos de coleta de lixo
No primeiro artigo desta série, apresentei dois métodos de coleta de lixo, ou seja, contagem de referências e coleta de rastreamento. A seguir, exploraremos mais detalhadamente essas duas abordagens e apresentaremos alguns algoritmos de coleta de rastreamento usados em ambientes de produção.
Coletor de contagem de referência
O coletor de contagem de referências registra o número de referências que apontam para cada objeto Java. Quando o número de referências que apontam para um objeto atinge 0, o objeto pode ser reciclado imediatamente. Esse imediatismo é a principal vantagem de um coletor com contagem de referências, e quase não há sobrecarga na manutenção da memória para a qual nenhuma referência aponta, mas manter o controle da última contagem de referências para cada objeto é caro.
A principal dificuldade do coletor de contagem de referências é como garantir a precisão da contagem de referências. Outra dificuldade bem conhecida é como lidar com referências circulares. Se dois objetos fizerem referência um ao outro e não forem referenciados por outros objetos ativos, a memória dos dois objetos nunca será recuperada porque o número de referências que apontam para nenhum dos objetos é 0. A reciclagem de memória de estruturas de referência circulares requer uma análise importante (Nota do tradutor: Análise global no heap Java), o que aumentará a complexidade do algoritmo e, portanto, trará sobrecarga adicional ao aplicativo.
coletor de rastreamento
O coletor de rastreamento é baseado na suposição de que todos os objetos vivos podem ser encontrados iterando referências (referências e referências de referências) a um conjunto inicial conhecido de objetos vivos. O conjunto inicial de objetos ativos (também chamados de objetos raiz) pode ser determinado analisando registros, objetos globais e quadros de pilha. Após determinar o conjunto inicial de objetos, o coletor de rastreamento segue os relacionamentos de referência desses objetos e marca os objetos apontados pelas referências como objetos ativos em sequência, de modo que o conjunto de objetos ativos conhecidos continue a se expandir. Este processo continua até que todos os objetos referenciados sejam marcados como objetos ativos e a memória dos objetos que não foram marcados seja recuperada.
O coletor de rastreamento difere do coletor de contagem de referência principalmente porque pode lidar com estruturas de referência circulares. A maioria dos coletores de rastreamento descobre objetos não referenciados em estruturas de referência circulares durante a fase de marcação.
O coletor de rastreamento é o método de gerenciamento de memória mais comumente usado em linguagens dinâmicas e atualmente é o método mais comum em Java. Também foi verificado em ambientes de produção por muitos anos. Abaixo apresentarei o coletor de rastreamento começando com alguns algoritmos para implementar a coleta de rastreamento.
Algoritmo de coleta de rastreamento
Coletores de lixo de cópia e coletores de lixo de varredura de marca não são novidade, mas ainda são os dois algoritmos mais comuns para implementar coleções de rastreamento atualmente.
Copiando o coletor de lixo
O coletor de lixo de cópia tradicional usa dois espaços de endereço no heap (ou seja, do espaço e para o espaço). Quando a coleta de lixo é executada, os objetos ativos no espaço de origem são copiados para o espaço de destino. são removidos (Nota do tradutor: Após copiar para o espaço to ou para a geração antiga), todo o espaço from pode ser reciclado. Quando o espaço for alocado novamente, o espaço to será usado primeiro (Nota do tradutor: Ou seja, o espaço to). da rodada anterior será usada como a nova rodada do espaço).
Na implementação inicial deste algoritmo, o espaço from e o espaço to mudaram continuamente suas posições, ou seja, quando o espaço to está cheio e a coleta de lixo é acionada, o espaço to se torna o espaço from, conforme mostrado na Figura 1. .
Figura 1 Sequência tradicional de coleta de lixo de cópia
O algoritmo de cópia mais recente permite que qualquer espaço de endereço no heap seja usado como espaço e a partir do espaço. Dessa forma, eles não precisam trocar de posição entre si, mas apenas mudar de posição logicamente.
A vantagem do coletor de cópia é que os objetos copiados no espaço são organizados de forma compacta e não há fragmentação alguma. A fragmentação é um problema comum enfrentado por outros coletores de lixo e também é o principal problema que discutirei mais tarde.
Desvantagens do coletor de cópias
De modo geral, o coletor de cópia é stop-the-world, o que significa que enquanto a coleta de lixo estiver em andamento, o aplicativo não poderá ser executado. Com esta implementação, quanto mais coisas você precisar copiar, maior será o impacto no desempenho do aplicativo. Esta é uma desvantagem para aplicativos que são sensíveis ao tempo de resposta. Ao usar o coletor de cópias, você também precisa considerar o pior cenário (ou seja, todos os objetos no espaço from são objetos ativos. Neste momento, você precisa preparar um espaço grande o suficiente para mover esses objetos ativos, para que o to). o espaço deve ser grande o suficiente para instalar todos os objetos no espaço. Devido a esta limitação, a utilização da memória do algoritmo de cópia é ligeiramente insuficiente (Nota do tradutor: Na pior das hipóteses, o espaço to precisa ter o mesmo tamanho que o espaço from, portanto, apenas 50% de utilização).
coletor de marcação clara
A maioria das JVMs comerciais implementadas em ambientes de produção empresarial usam um coletor de varredura de marcação (ou marcação) porque ele não replica o impacto de um coletor de lixo no desempenho do aplicativo. Os coletores de marcas mais famosos incluem CMS, G1, GenPar e DeterministicGC.
O coletor de varredura de marcação rastreia referências de objetos e marca cada objeto encontrado como ativo usando um bit de sinalização. Esse sinalizador geralmente corresponde a um endereço ou grupo de endereços no heap. Por exemplo: o bit ativo pode ser um bit no cabeçalho do objeto (Nota do tradutor: bit) ou um vetor de bits ou um bitmap.
Após a conclusão da marcação, entra-se na fase de limpeza. A fase de limpeza geralmente percorre o heap novamente (não apenas os objetos marcados como ativos, mas todo o heap) para localizar espaços de endereço de memória contíguos não marcados (a memória não marcada é livre e reciclável) e, em seguida, o coletor os organiza em listas livres. O coletor de lixo pode ter várias listas livres (geralmente divididas de acordo com o tamanho do bloco de memória). Alguns coletores JVM (por exemplo: JRockit Real Time) até dividem dinamicamente a lista livre com base na análise de desempenho do aplicativo e nas estatísticas de tamanho do objeto.
Após a fase de limpeza, o aplicativo poderá alocar memória novamente. Ao alocar memória para um novo objeto da lista livre, o bloco de memória recém-alocado precisa se ajustar ao tamanho do novo objeto, ou ao tamanho médio do objeto do thread, ou ao tamanho TLAB do aplicativo. Encontrar blocos de memória de tamanho adequado para novos objetos ajuda a otimizar a memória e reduzir a fragmentação.
Mark - Limpar defeitos de colecionador
O tempo de execução da fase de marcação depende do número de objetos ativos no heap, enquanto o tempo de execução da fase de limpeza depende do tamanho do heap. Portanto, para situações em que a configuração do heap é grande e há muitos objetos ativos no heap, o algoritmo de varredura de marca terá um determinado tempo de pausa.
Para aplicativos com uso intensivo de memória, você pode ajustar os parâmetros de coleta de lixo para atender a vários cenários e necessidades de aplicativos. Em muitos casos, esse ajuste pelo menos adia o risco representado pela fase de marcação/varredura para o SLA do aplicativo ou do contrato de serviço (o SLA aqui se refere ao tempo de resposta que o aplicativo precisa atingir). Mas o ajuste só é eficaz para cargas específicas e taxas de alocação de memória. Alterações de carga ou modificações no próprio aplicativo exigem um novo ajuste.
Implementação do coletor de varredura de marca
Existem pelo menos dois métodos comercialmente comprovados para implementar a coleta de lixo por varredura de marca. Uma é a coleta de lixo paralela e a outra é a coleta de lixo simultânea (ou na maioria das vezes simultânea).
Coletor paralelo
Coleta paralela significa que os recursos são usados em paralelo por threads de coleta de lixo. A maioria das implementações comerciais de coleta paralela são coletores de parar o mundo, nos quais todos os threads do aplicativo são pausados até que uma coleta de lixo seja concluída. Como os coletores de lixo podem usar recursos de maneira eficiente, eles geralmente têm melhor desempenho em benchmarks de rendimento, como. SPECjbb. Se a taxa de transferência for crítica para seu aplicativo, um coletor de lixo paralelo será uma boa opção.
O principal custo da coleta paralela (especialmente para ambientes de produção) é que os threads do aplicativo não podem funcionar adequadamente durante a coleta de lixo, assim como o coletor de cópia. Portanto, o uso de coletores paralelos terá um impacto significativo em aplicações sensíveis ao tempo de resposta. Especialmente quando há muitas estruturas complexas de objetos ativos no espaço de heap, há muitas referências de objetos que precisam ser rastreadas. (Lembre-se de que o tempo que leva para o coletor de varredura de marca recuperar a memória depende do tempo que leva para rastrear a coleção de objetos ativos mais o tempo que leva para percorrer todo o heap). Com a abordagem paralela, o aplicativo é pausado para o todo o tempo de coleta de lixo.
coletor simultâneo
Os coletores de lixo simultâneos são mais adequados para aplicativos sensíveis ao tempo de resposta. Simultaneidade significa que o encadeamento de coleta de lixo e o encadeamento do aplicativo são executados simultaneamente. O encadeamento de coleta de lixo não possui todos os recursos, portanto, ele precisa decidir quando iniciar uma coleta de lixo, permitindo tempo suficiente para rastrear a coleção de objetos ativos e recuperar a memória antes que a memória do aplicativo estoure. Se a coleta de lixo não for concluída a tempo, o aplicativo gerará um erro de estouro de memória. Por outro lado, você não deseja que a coleta de lixo demore muito, pois consumirá os recursos do aplicativo e afetará o rendimento. Manter esse equilíbrio requer habilidade, portanto, heurísticas são usadas para determinar quando iniciar a coleta de lixo e quando escolher otimizações para a coleta de lixo.
Outra dificuldade é determinar quando é seguro realizar algumas operações (operações que requerem um instantâneo de heap completo e preciso), como a necessidade de saber quando a fase de marcação está concluída para que a fase de limpeza possa ser iniciada. Isso não é um problema para um coletor paralelo de parar o mundo, porque o mundo já está pausado (Nota do tradutor: o encadeamento do aplicativo está pausado e o encadeamento de coleta de lixo monopoliza os recursos). Mas para coletores simultâneos, pode não ser seguro passar imediatamente da fase de marcação para a fase de limpeza. Se um thread de aplicativo modificar uma parte da memória que foi rastreada e marcada pelo coletor de lixo, novas referências não marcadas poderão ser geradas. Em algumas implementações de coleção simultânea, isso pode fazer com que a aplicação fique presa em um loop de anotações repetidas por um longo tempo sem conseguir obter memória livre quando a aplicação precisar desta memória.
A partir da discussão até agora, sabemos que existem muitos coletores de lixo e algoritmos de coleta de lixo, cada um adequado para tipos de aplicativos específicos e cargas diferentes. Não apenas algoritmos diferentes, mas diferentes implementações de algoritmos. Portanto, é melhor entender as necessidades da aplicação e suas próprias características antes de especificar um coletor de lixo. A seguir, apresentaremos algumas armadilhas do modelo de memória da plataforma Java. As armadilhas aqui referem-se a algumas suposições que os programadores Java estão propensos a fazer em um ambiente de produção em mudança dinâmica que piora o desempenho do aplicativo.
Por que o Tuning não pode substituir a coleta de lixo
A maioria dos programadores Java sabe que existem muitas opções para otimizar programas Java. Vários parâmetros opcionais de JVM, coletor de lixo e ajuste de desempenho permitem que os desenvolvedores gastem muito tempo em ajustes intermináveis de desempenho. Isso levou algumas pessoas a concluir que a coleta de lixo é ruim e que o ajuste para fazer com que as coletas de lixo ocorram com menos frequência ou durem menos é uma boa solução alternativa, mas é arriscado.
Considere o ajuste para um aplicativo específico. A maioria dos parâmetros de ajuste (como taxa de alocação de memória, tamanho do objeto, tempo de resposta) são baseados na taxa de alocação de memória do aplicativo (Nota do tradutor: ou outros parâmetros) com base no volume de dados de teste atual. Em última análise, pode levar aos dois resultados a seguir:
1. Um caso de uso aprovado no teste falha na produção.
2. Mudanças no volume de dados ou mudanças nos aplicativos exigem um novo ajuste.
O ajuste é iterativo e os coletores de lixo simultâneos, em particular, podem exigir muitos ajustes (especialmente em um ambiente de produção). Heurísticas são necessárias para atender às necessidades do aplicativo. Para atender ao pior cenário, o resultado do ajuste pode ser uma configuração muito rígida, o que também leva a muito desperdício de recursos. Essa abordagem de ajuste é uma busca quixotesca. Na verdade, quanto mais você otimiza o coletor de lixo para corresponder a uma carga específica, mais longe você fica da natureza dinâmica do tempo de execução Java. Afinal, quantos aplicativos têm uma carga estável e quão confiável você pode esperar que a carga seja?
Então, se você não está se concentrando no ajuste, o que pode fazer para evitar erros de falta de memória e melhorar os tempos de resposta? A primeira coisa é encontrar os principais fatores que afetam o desempenho das aplicações Java.
fragmentação
O fator que afeta o desempenho do aplicativo Java não é o coletor de lixo, mas a fragmentação e como o coletor de lixo lida com a fragmentação. A chamada fragmentação é um estado em que há espaço livre no heap, mas não há espaço de memória contíguo grande o suficiente para alocar memória para novos objetos. Conforme mencionado no primeiro artigo, a fragmentação da memória é um TLAB de espaço restante no heap ou o espaço ocupado por pequenos objetos que são liberados entre objetos de longa duração.
Com o tempo e à medida que o aplicativo é executado, essa fragmentação se espalha por todo o heap. Em alguns casos, o uso de parâmetros ajustados estaticamente pode ser pior porque eles não atendem às necessidades dinâmicas da aplicação. Os aplicativos não conseguem utilizar esse espaço fragmentado com eficiência. Deixar de fazer qualquer coisa resultará em coletas de lixo sucessivas, nas quais o coletor de lixo tenta liberar memória para alocação para novos objetos. Na pior das hipóteses, mesmo coletas de lixo sucessivas não conseguem liberar mais memória (muita fragmentação) e então a JVM precisa gerar um erro de estouro de memória. É possível resolver a fragmentação reiniciando o aplicativo para que o heap Java tenha espaço de memória contíguo para alocar novos objetos. Reiniciar o programa causa tempo de inatividade e, depois de um tempo, o heap Java ficará cheio de fragmentos novamente, forçando outra reinicialização.
Erros de falta de memória que interrompem o processo e os logs mostrando que o coletor de lixo está sobrecarregado indicam que a coleta de lixo está tentando liberar memória e que o heap está altamente fragmentado. Alguns programadores tentarão resolver o problema de fragmentação otimizando novamente o coletor de lixo. Mas penso que deveríamos encontrar formas mais inovadoras de resolver este problema. As seções a seguir focarão em duas soluções para fragmentação: coleta de lixo geracional e compactação.
Coleta de lixo geracional
Você deve ter ouvido a teoria de que a maioria dos objetos em um ambiente de produção tem vida curta. A coleta de lixo geracional é uma estratégia de coleta de lixo derivada dessa teoria. Na coleta de lixo geracional, dividimos o heap em diferentes espaços (ou gerações), e cada espaço armazena objetos de diferentes idades. A chamada idade de um objeto é o número de ciclos de coleta de lixo aos quais o objeto sobreviveu (ou seja,). qual a idade do objeto). ainda é referenciado após os ciclos de coleta de lixo).
Quando não houver espaço restante para alocar na nova geração, os objetos ativos na nova geração serão movidos para a geração antiga (geralmente há apenas duas gerações. Nota do tradutor: Somente objetos que atendem a uma determinada idade serão movidos para a geração antiga). geração antiga). Gerações A coleta de lixo geralmente usa um coletor de cópia unidirecional. Algumas JVMs mais modernas usam coletores paralelos na nova geração. É claro que diferentes algoritmos de coleta de lixo podem ser implementados para a nova geração e para a geração antiga. Se você usar um coletor paralelo ou um coletor copiador, seu jovem colecionador será um colecionador que pára o mundo (ver explicação anterior).
A geração antiga é alocada para objetos que foram movidos da nova geração. Esses objetos foram referenciados há muito tempo ou são referenciados por alguma coleção de objetos da nova geração. Ocasionalmente, objetos grandes são alocados diretamente à geração antiga porque o custo de movimentação de objetos grandes é relativamente alto.
Tecnologia geracional de coleta de lixo
Na coleta de lixo geracional, a coleta de lixo ocorre com menos frequência na geração antiga e com mais frequência na nova geração, e também esperamos que o ciclo de coleta de lixo na nova geração seja mais curto. Em casos raros, a geração mais jovem pode ser recolhida com mais frequência do que a geração mais velha. Isso pode acontecer se você tornar a geração mais jovem muito grande e a maioria dos objetos em seu aplicativo permanecerem ativos por muito tempo. Neste caso, se a geração antiga for demasiado pequena para acomodar todos os objectos de longa duração, a recolha de lixo da geração antiga também terá dificuldade em libertar espaço para os objectos que são movidos. No entanto, de modo geral, a coleta de lixo geracional pode permitir que os aplicativos obtenham melhor desempenho.
Outro benefício de dividir a nova geração é que isso resolve até certo ponto o problema da fragmentação, ou adia o pior cenário possível. Esses pequenos objetos com curto tempo de sobrevivência podem ter causado problemas de fragmentação, mas são todos eliminados na coleta de lixo de nova geração. Como os objetos de vida longa recebem um espaço mais compacto quando são movidos para a geração antiga, a geração antiga também é mais compacta. Com o tempo (se o seu aplicativo for executado por tempo suficiente), a geração antiga também se tornará fragmentada, exigindo a execução de uma ou mais coletas de lixo completas, e a JVM também poderá gerar erros de falta de memória. Mas criar uma nova geração adia o pior cenário, o que é suficiente para muitas aplicações. Para a maioria dos aplicativos, isso reduz a frequência da coleta de lixo do tipo stop-the-world e a chance de erros de falta de memória.
Otimize a coleta de lixo geracional
Como mencionado anteriormente, o uso da coleta de lixo geracional traz repetidos trabalhos de ajuste, como ajuste do tamanho da geração jovem, taxa de promoção, etc. Não posso enfatizar a desvantagem de um tempo de execução de aplicativo específico: escolher um tamanho fixo otimiza o aplicativo, mas também reduz a capacidade do coletor de lixo de lidar com mudanças dinâmicas, que são inevitáveis.
O primeiro princípio para a nova geração é aumentá-lo tanto quanto possível, garantindo ao mesmo tempo o tempo de atraso durante a coleta de lixo do tipo stop-the-world e, ao mesmo tempo, reservar espaço suficiente na pilha para objetos sobreviventes de longo prazo. Aqui estão alguns fatores adicionais a serem considerados ao ajustar um coletor de lixo geracional:
1. A maior parte da nova geração são coletores de lixo que param o mundo. Quanto maior a configuração da nova geração, maior será o tempo de pausa correspondente. Portanto, para aplicativos que são muito afetados pelos tempos de pausa da coleta de lixo, considere cuidadosamente o tamanho da geração mais jovem.
2. Diferentes algoritmos de coleta de lixo podem ser usados em diferentes gerações. Por exemplo, a coleta de lixo paralela é usada na geração mais jovem e a coleta de lixo simultânea é usada na geração mais antiga.
3. Quando se verifica que a promoção frequente (Nota do tradutor: passagem da nova geração para a antiga) falha, significa que há demasiados fragmentos na geração antiga, o que significa que não há espaço suficiente na geração antiga para armazenar objetos movidos da nova geração. Neste ponto você pode ajustar a taxa de promoção (ou seja, ajustar a idade da promoção) ou garantir que o algoritmo de coleta de lixo na geração antiga esteja fazendo a compactação (discutido no próximo parágrafo) e ajustar a compactação para se adequar à carga do aplicativo . Também é possível aumentar o tamanho do heap e o tamanho de cada geração, mas isso estenderá ainda mais o tempo de pausa na geração antiga. Saiba que a fragmentação é inevitável.
4. A coleta de lixo geracional é mais adequada para tais aplicações. Eles têm muitos objetos pequenos com tempos de sobrevivência curtos. Muitos objetos são reciclados na primeira rodada do ciclo de coleta de lixo. Para tais aplicações, a coleta de lixo geracional pode efetivamente reduzir a fragmentação e atrasar o impacto da fragmentação.
compressão
Embora a coleta de lixo geracional atrase a ocorrência de erros de fragmentação e de falta de memória, a compactação é a única solução real para o problema de fragmentação. A compactação é uma estratégia de coleta de lixo que libera blocos contíguos de memória movendo objetos, liberando espaço suficiente para criar novos objetos.
Mover objetos e atualizar referências de objetos são operações de parar o mundo que trarão uma certa quantidade de consumo (com uma exceção, que será discutida no próximo artigo desta série). Quanto mais objetos sobreviverem, maior será o tempo de pausa causado pela compactação. Em situações onde há pouco espaço restante e fragmentação severa (geralmente porque o programa está em execução há muito tempo), pode haver uma pausa de alguns segundos na compactação de áreas com muitos objetos vivos e ao se aproximar do estouro de memória, compactando o heap inteiro pode levar até dezenas de segundos.
O tempo de pausa para compactação depende da quantidade de memória que precisa ser movida e do número de referências que precisam ser atualizadas. A análise estatística mostra que quanto maior o heap, maior o número de objetos ativos que precisam ser movidos e as referências atualizadas. O tempo de pausa é de cerca de 1 segundo para cada 1 GB a 2 GB de objetos ativos movidos e, para um heap de 4 GB, é provável que haja 25% de objetos ativos, portanto, haverá pausas ocasionais de cerca de 1 segundo.
Parede de memória de compressão e aplicação
A parede de memória do aplicativo refere-se ao tamanho do heap que pode ser definido antes de uma pausa causada pela coleta de lixo (por exemplo: compactação). Dependendo do sistema e do aplicativo, a maioria das paredes de memória de aplicativos Java variam de 4 GB a 20 GB. É por isso que a maioria dos aplicativos corporativos são implementados em diversas JVMs menores, em vez de em algumas JVMs maiores. Vamos considerar o seguinte: quantos designs e implantações de aplicativos Java corporativos modernos são definidos pelas limitações de compactação da JVM. Nesse caso, para contornar o tempo de pausa da desfragmentação do heap, optamos por uma implantação de múltiplas instâncias que era mais cara de gerenciar. Isso é um pouco estranho, considerando os grandes recursos de armazenamento do hardware atual e a necessidade de maior memória para aplicativos Java de classe empresarial. Por que apenas alguns GB de memória são definidos para cada instância. A compactação simultânea quebrará a barreira da memória, que é o tópico do meu próximo artigo.
Resumir
Este artigo é um artigo introdutório sobre a coleta de lixo para ajudá-lo a compreender os conceitos e mecanismos da coleta de lixo e, esperançosamente, motivá-lo a ler mais artigos relacionados. Muitas das coisas discutidas aqui já existem há muito tempo e alguns novos conceitos serão introduzidos no próximo artigo. Por exemplo, a compactação simultânea é atualmente implementada pela JVM Zing da Azul. É uma tecnologia emergente de coleta de lixo que até tenta redefinir o modelo de memória Java, especialmente à medida que a memória e o poder de processamento continuam a melhorar hoje.
Aqui estão alguns pontos-chave sobre a coleta de lixo que resumi:
1. Diferentes algoritmos e implementações de coleta de lixo se adaptam às diferentes necessidades do aplicativo. O coletor de lixo de rastreamento é o coletor de lixo mais comumente usado em máquinas virtuais Java comerciais.
2. A coleta de lixo paralela usa todos os recursos em paralelo ao realizar a coleta de lixo. Geralmente é um coletor de lixo que pára o mundo e, portanto, tem maior rendimento, mas os threads de trabalho do aplicativo devem aguardar a conclusão do thread de coleta de lixo, o que tem um certo impacto no tempo de resposta do aplicativo.
3. Coleta de lixo simultânea: enquanto a coleta está sendo executada, o thread de trabalho do aplicativo ainda está em execução. Um coletor de lixo simultâneo precisa concluir a coleta de lixo antes que o aplicativo precise de memória.
4. A coleta de lixo geracional ajuda a retardar a fragmentação, mas não pode eliminá-la. A coleta de lixo geracional divide o heap em dois espaços, um espaço para novos objetos e outro para objetos antigos. A coleta de lixo geracional é adequada para aplicações com muitos objetos pequenos que têm vida útil curta.
5. A compactação é a única maneira de resolver a fragmentação. A maioria dos coletores de lixo executa a compactação de uma maneira que interrompe o mundo. Quanto mais tempo o programa for executado, mais complexas serão as referências aos objetos e mais desigualmente distribuídos serão os tamanhos dos objetos, o que levará a tempos de compactação mais longos. O tamanho do heap também afeta o tempo de compactação, pois pode haver mais objetos ativos e referências que precisam ser atualizadas.
6. O ajuste ajuda a retardar erros de estouro de memória. Mas o resultado do ajuste excessivo é uma configuração rígida. Antes de começar a ajustar por meio de uma abordagem de tentativa e erro, certifique-se de compreender a carga no seu ambiente de produção, os tipos de objeto do seu aplicativo e as características das suas referências de objeto. Configurações muito rígidas podem não ser capazes de lidar com cargas dinâmicas; portanto, compreenda as consequências ao definir valores não dinâmicos.
O próximo artigo desta série é: Uma discussão aprofundada sobre o algoritmo de coleta de lixo C4 (Concurrent Continuously Compacting Collector), portanto, fique atento!
(O texto completo termina)