java - sueldo - Estrategia agresiva del recolector de basura.
recolector de basura sueldo (4)
Estoy ejecutando una aplicación que crea y olvida grandes cantidades de objetos, la cantidad de objetos largos que existen crece lentamente, pero esto es muy poco en comparación con los objetos de vida corta. Esta es una aplicación de escritorio con requisitos de alta disponibilidad, debe estar encendida las 24 horas del día. La mayor parte del trabajo se realiza en un solo subproceso, este subproceso solo usará toda la CPU que pueda obtener en sus manos.
En el pasado, hemos visto lo siguiente bajo una gran carga: el espacio del montón utilizado aumenta lentamente a medida que el recolector de basura recolecta menos que la cantidad de memoria recién asignada, el tamaño del montón utilizado crece lentamente y eventualmente se acerca al máximo del montón especificado. En ese momento, el recolector de basura iniciará pesadamente y comenzará a utilizar una gran cantidad de recursos para evitar superar el tamaño máximo de almacenamiento dinámico. Esto ralentiza la aplicación (fácilmente 10 veces más lento) y en este punto, la mayoría de las veces, el GC logrará limpiar la basura después de unos minutos o fallará y lanzará una OutOfMemoryException
, ambos no son realmente aceptables.
El hardware utilizado es un procesador de cuatro núcleos con al menos 4 GB de memoria que ejecuta Linux de 64 bits, todo lo que podemos usar si es necesario. Actualmente, la aplicación utiliza mucho un solo núcleo, que está utilizando la mayor parte del tiempo ejecutando un solo núcleo / subproceso. La mayoría de los otros núcleos están inactivos y podrían utilizarse para la recolección de basura.
Tengo la sensación de que el recolector de basura debería estar recolectando más agresivamente en una etapa temprana, mucho antes de que se quede sin memoria. Nuestra aplicación no tiene problemas de rendimiento, los requisitos de tiempo de pausa baja son un poco más importantes que el rendimiento, pero mucho menos importantes que no acercarse al tamaño máximo de almacenamiento dinámico. Es aceptable si el único hilo ocupado se ejecuta a solo el 75% de la velocidad actual, siempre que el recolector de basura pueda mantenerse al día con la creación. En resumen, una disminución constante del rendimiento es mejor que la caída repentina que vemos ahora.
He leído Java SE 6 HotSpot [tm] Sintonización de recolección de basura de máquina virtual a fondo, lo que significa que entiendo bien las opciones, sin embargo, todavía me resulta difícil elegir la configuración correcta ya que mis requisitos son un poco diferentes de lo que se explica en el documento .
Actualmente estoy usando ParallelGC con la opción -XX:GCTimeRatio=4
. Esto funciona un poco mejor que la configuración predeterminada para la relación de tiempo, pero tengo la sensación de que el GC puede funcionar más con esa configuración de lo que lo hace.
Para el monitoreo estoy usando jconsole y jvisualvm principalmente.
Me gustaría saber qué opciones de recolección de basura recomienda para la situación anterior. También puedo ver qué salida de depuración de GC para entender mejor el cuello de la botella.
EDITAR: Entiendo que una muy buena opción aquí es crear menos basura, esto es algo que realmente estamos considerando, sin embargo, me gustaría saber cómo podemos abordar esto con el ajuste de GC, ya que es algo que podemos hacer mucho más fácilmente y rodar Salir más rápido que cambiar grandes cantidades del código fuente. También he ejecutado los diferentes perfiladores de memoria y entiendo para qué sirve la basura, y allí sé que consiste en objetos que se podrían recolectar.
Estoy usando:
java version "1.6.0_27-ea"
Java(TM) SE Runtime Environment (build 1.6.0_27-ea-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.2-b03, mixed mode)
Con parámetros JVM:
-Xmx1024M and -XX:GCTimeRatio=4
Editar en respuesta a los comentarios de Matts: la mayoría de la memoria (y cpu) se dirige a la construcción de objetos que representan la situación actual. Algunos de estos se descartarán de inmediato a medida que la situación cambie rápidamente, otros tendrán una vida media si no se reciben actualizaciones durante un tiempo.
El algoritmo G1GC
, que se ha introducido con Java 1.7
estable, está funcionando bien. Debe especificar el tiempo máximo de pausa con el que desea vivir en su aplicación. JVM se encargará de todas las demás cosas por ti.
Parámetros clave:
-XX:+UseG1GC -XX:MaxGCPauseMillis=1000
Hay algunos parámetros más para configurar. Si está utilizando 4 GB de RAM, configure el tamaño de la región como 4 GB / 2048 bloques, que es aproximadamente 2 MB
-XX:G1HeapRegionSize=2
Si tiene 8 CPU centrales, ajuste dos parámetros más
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2
Aparte de estos parámetros, deje los valores de otros parámetros por defecto como
-XX:TargetSurvivorRatio
etc.
Echa un vistazo a la G1GC de G1GC
para obtener más detalles sobre G1GC
.
-XX:G1HeapRegionSize=n
Establece el tamaño de una región G1. El valor será una potencia de dos y puede variar de 1 MB a 32 MB. El objetivo es tener alrededor de 2048 regiones basadas en el tamaño mínimo de almacenamiento dinámico de Java.
-XX:MaxGCPauseMillis=200
Establece un valor objetivo para el tiempo de pausa máximo deseado. El valor predeterminado es 200 milisegundos. El valor especificado no se adapta al tamaño de su montón.
-XX:ParallelGCThreads=n
Establece el valor de los hilos de trabajo STW. Establece el valor de n al número de procesadores lógicos. El valor de n es el mismo que el número de procesadores lógicos hasta un valor de 8.
Si hay más de ocho procesadores lógicos, establece el valor de n en aproximadamente 5/8 de los procesadores lógicos. Esto funciona en la mayoría de los casos, excepto en sistemas SPARC más grandes donde el valor de n puede ser aproximadamente 5/16 de los procesadores lógicos.
-XX:ConcGCThreads=n
Recomendaciones de Oráculo:
Cuando evalúe y ajuste el G1 GC, tenga en cuenta las siguientes recomendaciones:
Tamaño de generación joven : evite establecer explícitamente el tamaño de generación joven con la opción
-Xmn
o cualquier otra opción relacionada, como-XX:NewRatio
.Fixing the size of the young generation overrides the target pause-time goal
.Objetivos de tiempo de pausa: cuando evalúa o ajusta cualquier recolección de basura, siempre hay una compensación entre latencia y rendimiento. El G1 GC es un recolector de basura incremental con pausas uniformes, pero también más sobrecarga en los hilos de la aplicación.
The throughput goal for the G1 GC is 90 percent application time and 10 percent garbage collection time
.
Recientemente, he reemplazado CMS con el algoritmo G1GC para un montón de 4 GB con división casi igual de generación joven y antigua. He establecido el tiempo de MaxGCPause
y los resultados son impresionantes.
Las primeras opciones de VM que intentaría son aumentar NewSize
y MaxNewSize
y usar uno de los algoritmos paralelos de GC (intente UseConcMarkSweepGC, que está diseñado para "mantener cortas las pausas de recolección de basura").
Para confirmar que las pausas que está viendo se deben a GC, active el registro detallado de GC ( -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
). Más información sobre cómo leer estos registros está disponible en online .
Para comprender el cuello de botella, ejecute la aplicación en un generador de perfiles. Tome una instantánea de montón Entonces, deja que la aplicación haga su trabajo por un tiempo. Tome otra instantánea montón. Para ver lo que ocupa todo el espacio, busque lo que haya mucho más después de la segunda instantánea del montón. Visual VM puede hacer esto, pero también considerar MAT .
Como alternativa, considere usar -XX:+HeapDumpOnOutOfMemoryError
para obtener una instantánea del problema real, y no tiene que reproducirlo en otro entorno. El montón guardado se puede analizar con las mismas herramientas: MAT, etc.
Sin embargo, es posible que esté obteniendo una OutOfMemoryException
porque tiene una pérdida de memoria o porque está ejecutando con un tamaño de almacenamiento dinámico demasiado pequeño. El registro detallado de GC debería ayudarlo a responder ambas preguntas.
No mencionas qué compilación de JVM estás ejecutando, esta es información crucial. Tampoco menciona por cuánto tiempo la aplicación tiende a ejecutarse (por ejemplo, ¿es por la duración de un día laborable? ¿Una semana? ¿Menos?)
Algunos otros puntos
- Si continuamente está perdiendo objetos en tenencia porque está asignando a un ritmo más rápido que su gen joven puede ser barrido, entonces sus generaciones tienen un tamaño incorrecto. Necesitará realizar un análisis adecuado del comportamiento de su aplicación para poder dimensionarla correctamente, puede usar visualgc para esto.
- el colector de rendimiento está diseñado para aceptar una pausa grande y única en lugar de muchas pausas más pequeñas, el beneficio es que es un colector compacto y permite un rendimiento total más alto
- CMS existe para servir al otro extremo del espectro, es decir, muchas más pausas mucho más pequeñas pero un rendimiento total más bajo. El inconveniente es que no se compacta, por lo que la fragmentación puede ser un problema. El problema de la fragmentación se mejoró en 6u26, por lo que si no estás en esa compilación, puede ser el momento de la actualización. Tenga en cuenta que el efecto de "sangrado en tenencia" que ha observado en el problema exacerba el problema de la fragmentación y, dado el tiempo, esto dará lugar a fallos en la promoción (también conocido como gc completo y asociado a la pausa STW). Anteriormente he escrito una respuesta sobre esto en esta pregunta.
- Si está ejecutando una JVM de 64 bits con> 4 GB de RAM y una JVM lo suficientemente reciente, asegúrese de que
-XX:+UseCompressedOops
contrario, simplemente está desperdiciando espacio, ya que una JVM de 64 bits ocupa ~ 1.5 veces el espacio de una JVM de 32 bits para la misma carga de trabajo sin él (y si no lo está, actualice para obtener acceso a más RAM)
- Si está ejecutando una JVM de 64 bits con> 4 GB de RAM y una JVM lo suficientemente reciente, asegúrese de que
Es posible que también desee leer otra respuesta que he escrito sobre este tema, que tiene que ver con el tamaño adecuado de sus espacios de sobrevivientes y eden. Básicamente lo que quieres lograr es;
- Eden lo suficientemente grande como para que no se recoja con demasiada frecuencia.
- espacios de sobrevivientes dimensionados para coincidir con el umbral de tenencia
- un umbral de permanencia establecido para garantizar, en la medida de lo posible, que solo los objetos verdaderamente longevos se conviertan en tenencia
Por lo tanto, si tiene un montón de 6G, podría hacer algo como 5G eden + 16M espacios de sobrevivientes + un umbral de permanencia de 1.
El proceso básico es
- asignar en eden
- eden se llena
- Objetos vivos barridos en el espacio para sobrevivir
- objetos vivos desde el espacio de sobrevivientes copiados al espacio o promovidos a tenencia (según el umbral de tenencia y el espacio disponible y ninguna de las veces que se han copiado de 1 a otro)
- todo lo que queda en eden es barrido
Por lo tanto, dados los espacios del tamaño adecuado para el perfil de asignación de su aplicación, es perfectamente posible configurar el sistema de manera que maneje la carga de manera adecuada. Algunas advertencias a esto;
- necesita algunas pruebas de ejecución prolongada para hacer esto correctamente (por ejemplo, puede llevar días solucionar el problema de fragmentación del CMS)
- Necesitas hacer cada prueba varias veces para obtener buenos resultados.
- necesitas cambiar 1 cosa a la vez en la configuración de GC
- debe poder presentar una carga de trabajo razonablemente repetible a la aplicación, de lo contrario será difícil comparar objetivamente los resultados de diferentes ejecuciones de prueba
- esto será realmente difícil de hacer de manera confiable si la carga de trabajo es impredecible y tiene picos / depresiones masivas
Los puntos 1 a 3 significan que esto puede tardar años en llegar bien. Por otro lado, puede ser capaz de hacerlo lo suficientemente bueno v rápidamente, ¡depende de lo anal que seas!
Finalmente, haciendo eco del punto de Peter Lawrey, puede ahorrar una gran cantidad de molestias (aunque presente alguna otra molestia) si es realmente riguroso con respecto a la asignación de objetos.
Puedes intentar reducir el nuevo tamaño. Esto lo hará para hacer más, colecciones más pequeñas. Sin embargo, puede hacer que estos objetos de corta duración pasen al espacio de tenencia. Por otro lado, puede intentar aumentar NewSize, lo que significa que menos objetos pasarán de la generación joven.
Sin embargo, mi preferencia es crear menos basura y el GC se comportará de una manera más consistente. En lugar de crear objetos libremente, intente reutilizarlos o reciclar objetos. Debe tener cuidado, ya que esto no causa más problemas de los que vale, pero puede reducir la cantidad de basura creada de manera significativa en algunas aplicaciones. Sugiero usar un generador de perfiles de memoria, por ejemplo, YourKit para ayudarte a identificar a los más grandes bateadores.
Un caso extremo es crear una basura tan pequeña que no se recoja en todo el día (incluso las colecciones menores). Es posible para una aplicación del lado del servidor (pero puede no ser posible para una aplicación GUI)