Accidentalmente descubrí la definición de "Cerdo en Python (Nota: es un poco como la serpiente codiciosa e insuficiente que se traga al elefante)" en chino cuando estaba mirando el glosario de administración de memoria, así que se me ocurrió este artículo. En la superficie, este término se refiere a que GC promueve constantemente objetos grandes de una generación a otra. Hacer esto es como si una pitón se tragara entera a su presa, de modo que no pueda moverse mientras la digiere.
Durante las siguientes 24 horas, mi mente se llenó de imágenes de esta pitón asfixiante de la que no podía deshacerme. Como dicen los psiquiatras, la mejor manera de aliviar el miedo es hablarlo. De ahí este artículo. Pero la siguiente historia de la que queremos hablar no es Python, sino el ajuste de GC. Lo juro por Dios.
Todo el mundo sabe que las pausas de GC pueden provocar fácilmente cuellos de botella en el rendimiento. Las JVM modernas vienen con recolectores de basura avanzados cuando se lanzan, pero según mi experiencia, es extremadamente difícil encontrar la configuración óptima para una determinada aplicación. Es posible que el ajuste manual aún tenga un rayo de esperanza, pero es necesario comprender la mecánica exacta del algoritmo GC. En este sentido, este artículo le resultará útil. A continuación, utilizaré un ejemplo para explicar cómo un pequeño cambio en la configuración de JVM afecta el rendimiento de su aplicación.
Ejemplo
La aplicación que utilizamos para demostrar el impacto de GC en el rendimiento fue un programa simple. Contiene dos hilos:
PigEater imitará el proceso de una pitón gigante comiéndose a un cerdo grande y gordo. El código hace esto agregando 32 MB de bytes a java.util.List y durmiendo durante 100 ms después de cada trago.
PigDgester Simula el proceso de digestión asincrónica. El código que implementa la digestión simplemente establece que la lista de cerdos esté vacía. Dado que este es un proceso agotador, este hilo dormirá durante 2000 ms cada vez después de borrar la referencia.
Ambos hilos se ejecutarán en un bucle while, comiendo y digiriendo hasta que la serpiente esté llena. Esto requeriría comer aproximadamente 5.000 cerdos.
Copie el código de código de la siguiente manera:
paquete eu.plumbr.demo;
clase pública PigInThePython {
Cerdos de lista volátil estática = new ArrayList();
static volatile int cerdosEaten = 0;
int final estático ENOUGH_PIGS = 5000;
public static void main (String [] args) lanza InterruptedException {
nuevo PigEater().start();
nuevo PigDigester().start();
}
clase estática PigEater extiende Thread {
@Anular
ejecución pública vacía() {
mientras (verdadero) {
pigs.add(new byte[32 * 1024 * 1024]); //32 MB por cerdo
si (cerdosEaten > ENOUGH_PIGS) regresa;
tomarANap(100);
}
}
}
La clase estática PigDigester extiende el hilo {
@Anular
ejecución pública vacía() {
inicio largo = System.currentTimeMillis();
mientras (verdadero) {
tomarANap(2000);
cerdosComidos+=cerdos.tamaño();
cerdos = nueva ArrayList();
if (cerdoscomidos > SUFICIENTE_PIGS) {
System.out.format("%d cerdos digeridos en %d ms.%n",pigsEaten, System.currentTimeMillis()-start);
devolver;
}
}
}
}
vacío estático takeANap(int ms) {
intentar {
Thread.sleep(ms);
} captura (Excepción e) {
e.printStackTrace();
}
}
}
Ahora definimos el rendimiento de este sistema como "el número de cerdos que se pueden digerir por segundo". Teniendo en cuenta que se introduce un cerdo en esta pitón cada 100 ms, podemos ver que el rendimiento máximo teórico de este sistema puede alcanzar 10 cerdos/segundo.
Ejemplo de configuración del GC
Echemos un vistazo al rendimiento al usar dos sistemas de configuración diferentes. Independientemente de la configuración, la aplicación se ejecuta en una Mac de doble núcleo (OS X10.9.3) con 8 GB de RAM.
Primera configuración:
Montón de 1,4G (-Xms4g -Xmx4g)
2. Use CMS para limpiar la generación anterior (-XX: + UseConcMarkSweepGC) y use el recopilador paralelo para limpiar la nueva generación (-XX: + UseParNewGC)
3. Asigne el 12,5% del montón (-Xmn512m) a la nueva generación y limite los tamaños del área de Eden y el área de Survivor para que sean iguales.
La segunda configuración es ligeramente diferente:
Montón de 1,2G (-Xms2g -Xms2g)
2. Tanto la nueva generación como la antigua utilizan Parallel GC (-XX: + UseParallelGC)
3. Asignar el 75% del montón a la nueva generación (-Xmn 1536m)
4. Ahora es el momento de hacer una apuesta: ¿qué configuración funcionará mejor (cuántos cerdos se pueden comer por segundo, recuerda)? Aquellos que pongan sus chips en la primera configuración, quedarán decepcionados. El resultado es todo lo contrario:
1. La primera configuración (montón grande, generación anterior grande, CMS GC) puede consumir 8,2 cerdos por segundo
2. La segunda configuración (montón pequeño, nueva generación grande, GC paralelo) puede consumir 9,2 cerdos por segundo.
Ahora veamos este resultado objetivamente. Los recursos asignados son 2 veces menores pero el rendimiento aumenta en un 12%. Esto va en contra del sentido común, por lo que es necesario analizar más a fondo lo que está pasando.
Analizar los resultados de GC
En realidad, la razón no es complicada. Puede encontrar la respuesta observando más de cerca lo que hace el GC al ejecutar la prueba. Aquí es donde eliges la herramienta que deseas utilizar. Con la ayuda de jstat, descubrí el secreto detrás de esto. El comando probablemente sea así:
Copie el código de código de la siguiente manera:
jstat -gc -t -h20 PID 1s
Al analizar los datos, noté que la configuración 1 pasó por 1129 ciclos de GC (YGCT_FGCT), lo que tomó un total de 63,723 segundos:
Copie el código de código de la siguiente manera:
Marca de tiempo 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
La segunda configuración se detuvo un total de 168 veces (YGCT+FGCT) y solo tomó 11,409 segundos.
Copie el código de código de la siguiente manera:
Marca de tiempo 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
Teniendo en cuenta que la carga de trabajo en ambos casos es igual, en este experimento de comer cerdos, cuando el GC no encuentra objetos de larga duración, puede limpiar los objetos basura más rápido. Con la primera configuración, la frecuencia de funcionamiento del GC será de 6 a 7 veces y el tiempo total de pausa será de 5 a 6 veces.
Contar esta historia tiene dos propósitos. Primero y más importante, quería sacarme de la cabeza esta pitón convulsionada. Otro beneficio más obvio es que el ajuste de GC es una experiencia muy hábil y requiere una comprensión profunda de los conceptos subyacentes. Aunque la que se utiliza en este artículo es solo una aplicación muy común, los diferentes resultados de la selección también tendrán un gran impacto en su rendimiento y planificación de capacidad. En aplicaciones de la vida real, la diferencia aquí será aún mayor. Depende de usted si puede dominar estos conceptos o simplemente concentrarse en su trabajo diario y dejar que Plumbr descubra la configuración de GC más adecuada para sus necesidades.