Eu acidentalmente descobri a definição de "Porco no Python (Nota: é um pouco como a cobra gananciosa e insuficiente engolindo o elefante)" em chinês quando estava olhando o glossário de gerenciamento de memória, então criei este artigo. Superficialmente, este termo se refere ao GC promovendo constantemente objetos grandes de uma geração para outra. Fazer isso é como se uma píton engolisse sua presa inteira, de modo que não pudesse se mover enquanto a digere.
Nas 24 horas seguintes, minha mente ficou repleta de imagens dessa píton sufocante da qual eu não conseguia me livrar. Como dizem os psiquiatras, a melhor maneira de aliviar o medo é conversar. Daí este artigo. Mas a próxima história sobre a qual queremos falar não é sobre python, mas sobre ajuste de GC. Juro por Deus.
Todo mundo sabe que as pausas do GC podem facilmente causar gargalos de desempenho. As JVMs modernas vêm com coletores de lixo avançados quando são lançadas, mas, pela minha experiência, é extremamente difícil encontrar a configuração ideal para um determinado aplicativo. O ajuste manual ainda pode ter um vislumbre de esperança, mas você precisa entender a mecânica exata do algoritmo de GC. Nesse sentido, este artigo será útil para você. Abaixo usarei um exemplo para explicar como uma pequena alteração na configuração da JVM afeta o rendimento do seu aplicativo.
Exemplo
O aplicativo que usamos para demonstrar o impacto do GC no rendimento foi um programa simples. Ele contém dois tópicos:
PigEater Imitará o processo de uma píton gigante comendo um porco grande e gordo. O código faz isso adicionando 32 MB de bytes a java.util.List e dormindo por 100 ms após cada ingestão.
PigDgester Simula o processo de digestão assíncrona. O código que implementa a digestão simplesmente define a lista de porcos como vazia. Como este é um processo cansativo, esse thread irá dormir por 2.000 ms a cada vez após limpar a referência.
Ambos os threads serão executados em um loop while, comendo e digerindo até que a cobra esteja cheia. Isso exigiria comer aproximadamente 5.000 porcos.
Copie o código do código da seguinte forma:
pacote eu.plumbr.demo;
classe pública PigInThePython {
lista estática volátil porcos = new ArrayList();
estático volátil int porcosComidos = 0;
final estático int ENOUGH_PIGS = 5000;
public static void main(String[] args) lança InterruptedException {
new PigEater().start();
novo PigDigester().start();
}
classe estática PigEater estende Thread {
@Substituir
execução void pública() {
enquanto (verdadeiro) {
pigs.add(novo byte[32 * 1024 * 1024]); //32 MB por porco
if (pigsEaten > ENOUGH_PIGS) retornar;
pegueANap(100);
}
}
}
classe estática PigDigester estende Thread {
@Substituir
execução void pública() {
início longo = System.currentTimeMillis();
enquanto (verdadeiro) {
pegueANap(2000);
porcosComidos+=porcos.size();
porcos = new ArrayList();
if (porcoComido > ENOUGH_PIGS) {
System.out.format("%d porcos digeridos em %d ms.%n",pigsEaten, System.currentTimeMillis()-start);
retornar;
}
}
}
}
estático void takeANap(int ms) {
tentar {
Thread.sleep(ms);
} catch (Exceção e) {
e.printStackTrace();
}
}
}
Agora definimos o rendimento deste sistema como “o número de porcos que podem ser digeridos por segundo”. Considerando que um porco é colocado neste python a cada 100ms, podemos ver que o rendimento máximo teórico deste sistema pode chegar a 10 porcos/segundo.
Exemplo de configuração de GC
Vamos dar uma olhada no desempenho do uso de dois sistemas de configuração diferentes. Independentemente da configuração, o aplicativo roda em um Mac dual-core (OS X10.9.3) com 8GB de RAM.
Primeira configuração:
Pilha de 1,4 G (-Xms4g -Xmx4g)
2. Use o CMS para limpar a geração antiga (-XX:+UseConcMarkSweepGC) e use o coletor paralelo para limpar a nova geração (-XX:+UseParNewGC)
3. Aloque 12,5% do heap (-Xmn512m) para a nova geração e limite os tamanhos da área Eden e da área Survivor para que sejam iguais.
A segunda configuração é um pouco diferente:
Pilha de 1,2 G (-Xms2g -Xms2g)
2. Tanto a nova geração quanto a antiga usam Parellel GC (-XX:+UseParallelGC)
3. Aloque 75% do heap para a nova geração (-Xmn 1536m)
4. Agora é hora de apostar, qual configuração terá melhor desempenho (quantos porcos podem ser comidos por segundo, lembra)? Quem apostar suas fichas na primeira configuração ficará desapontado. O resultado é exatamente o oposto:
1. A primeira configuração (pilha grande, geração antiga grande, CMS GC) pode comer 8,2 porcos por segundo
2. A segunda configuração (pequena pilha, grande nova geração, Parellel GC) pode comer 9,2 porcos por segundo
Agora vamos examinar esse resultado objetivamente. Os recursos alocados são 2 vezes menores, mas o rendimento aumenta em 12%. Isto é contrário ao bom senso, por isso é necessário analisar melhor o que está acontecendo.
Analise os resultados do GC
Na verdade, o motivo não é complicado. Você pode encontrar a resposta observando mais de perto o que o GC está fazendo ao executar o teste. É aqui que você escolhe a ferramenta que deseja usar. Com a ajuda do jstat, descobri o segredo por trás disso. O comando é provavelmente assim:
Copie o código do código da seguinte forma:
jstat -gc -t -h20 PID 1s
Analisando os dados, notei que a configuração 1 passou por 1129 ciclos de GC (YGCT_FGCT), demorando um total de 63,723 segundos:
Copie o código do código da seguinte forma:
Carimbo de data/hora S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
594,0 174720,0 174720,0 163844,1 0,0 174848,0 131074,1 3670016,0 2621693,5 21248,0 2580,9 1006 63,182 116 0,236 63.419
595,0 174720,0 174720,0 163842,1 0,0 174848,0 65538,0 3670016,0 3047677,9 21248,0 2580,9 1008 63,310 117 0,236 63.546
596,1 174720,0 174720,0 98308,0 163842,1 174848,0 163844,2 3670016,0 491772,9 21248,0 2580,9 1010 63,354 118 0,240 63.595
597,0 174720,0 174720,0 0,0 163840,1 174848,0 131074,1 3670016,0 688380,1 21248,0 2580,9 1011 63,482 118 0,240 63.723
A segunda configuração pausou um total de 168 vezes (YGCT+FGCT) e demorou apenas 11.409 segundos.
Copie o código do código da seguinte forma:
Carimbo de data/hora S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
539,3 164352,0 164352,0 0,0 0,0 1211904,0 98306,0 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
540,3 164352,0 164352,0 0,0 0,0 1211904,0 425986,2 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
541,4 164352,0 164352,0 0,0 0,0 1211904,0 720900,4 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
542,3 164352,0 164352,0 0,0 0,0 1211904,0 1015812,6 524288,0 164352,2 21504,0 2579,2 27 2,969 141 8,441 11,409
Considerando que a carga de trabalho em ambos os casos é igual, portanto - neste experimento de comer porcos, quando o GC não encontra objetos de vida longa, ele pode limpar objetos de lixo mais rapidamente. Com a primeira configuração, a frequência de operação do GC será de cerca de 6 a 7 vezes e o tempo total de pausa será de 5 a 6 vezes.
Contar essa história serve a dois propósitos. Em primeiro lugar e mais importante, eu queria tirar essa píton em convulsão da minha mente. Outro ganho mais óbvio é que o ajuste do GC é uma experiência muito habilidosa e exige que você tenha um conhecimento completo dos conceitos subjacentes. Embora o usado neste artigo seja apenas uma aplicação muito comum, os diferentes resultados da seleção também terão um grande impacto no seu rendimento e planejamento de capacidade. Em aplicações da vida real, a diferença aqui será ainda maior. Portanto, depende de você dominar esses conceitos ou apenas se concentrar em seu trabalho diário e deixar a Plumbr descobrir a configuração de GC mais adequada para suas necessidades.