java - check - ¿Por qué Sun JVM continúa consumiendo memoria RSS aún más cuando los tamaños de pila, etc. son estables?
jvm parameters (4)
Durante el año pasado, realicé grandes mejoras en el uso del montón de aplicaciones Java, una sólida reducción del 66%. En la búsqueda de eso, he estado monitoreando varias métricas, como el tamaño de almacenamiento dinámico de Java, la CPU, Java no-montón, etc. a través de SNMP.
Recientemente, he estado controlando la cantidad de memoria real (RSS, residente establecido) por la JVM y estoy algo sorprendido. La memoria real consumida por la JVM parece totalmente independiente de mis aplicaciones de tamaño de almacenamiento dinámico, no almacenamiento dinámico, espacio de edredón, conteo de hilos, etc.
Heap Size según lo medido por Java SNMP Java Heap Used Graph http://lanai.dietpizza.ch/images/jvm-heap-used.png
Memoria real en KB. (Por ejemplo: 1 MB de KB = 1 GB) Gráfico usado Java Heap http://lanai.dietpizza.ch/images/jvm-rss.png
(Los tres saltos en el gráfico de montón corresponden a las actualizaciones / reinicios de la aplicación).
Esto es un problema para mí porque toda esa memoria extra que consume la JVM es "robar" la memoria que podría ser utilizada por el sistema operativo para el almacenamiento en caché de archivos. De hecho, una vez que el valor de RSS alcanza ~ 2.5-3GB, empiezo a ver tiempos de respuesta más lentos y una mayor utilización de la CPU de mi aplicación, principalmente a IO wait. Como punto de inicio de paginación a la partición de intercambio. Todo esto es muy indeseable.
Entonces, mis preguntas:
- ¿Por qué está pasando esto? ¿Qué está pasando "debajo del capó" ?
- ¿Qué puedo hacer para mantener el consumo de memoria real de la JVM bajo control?
Los detalles sangrientos:
- RHEL4 de 64 bits (Linux - 2.6.9-78.0.5.ELsmp # 1 SMP Wed sep 24 ... 2008 x86_64 ... GNU / Linux)
- Java 6 (compilación 1.6.0_07-b06)
- Tomcat 6
- Aplicación (transmisión de video HTTP a pedido)
- Alta E / S a través de java.nio FileChannels
- Cientos a bajos miles de hilos
- Bajo uso de base de datos
- Primavera, hibernar
Parámetros relevantes de JVM:
-Xms128m
-Xmx640m
-XX:+UseConcMarkSweepGC
-XX:+AlwaysActAsServerClassMachine
-XX:+CMSIncrementalMode
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+CMSLoopWarn
-XX:+HeapDumpOnOutOfMemoryError
Cómo mido RSS:
ps x -o command,rss | grep java | grep latest | cut -b 17-
Esto va a un archivo de texto y se lee en una base de datos RRD en el sistema de monitoreo en intervalos regulares. Tenga en cuenta que ps genera Kilo Bytes.
El problema y la solución s :
Mientras que al final fue la respuesta de ATorras la que finalmente resultó correcta, kdgregory me guió a la ruta de diagnóstico correcta con el uso de pmap
. (¡Ve a votar por sus respuestas!) Esto es lo que estaba sucediendo:
Cosas que sé con certeza:
- Mi aplicación registra y muestra datos con JRobin 1.4 , algo que codifiqué en mi aplicación hace más de tres años.
- La instancia más activa de la aplicación actualmente crea
- Más de 1000 nuevos archivos de base de datos de JRobin (aproximadamente 1,3 MB cada uno) una hora después de iniciarse
- ~ 100 + cada día después de la puesta en marcha
- La aplicación actualiza estos objetos de la base de datos JRobin una vez cada 15 s, si hay algo que escribir.
- En la configuración predeterminada JRobin:
- utiliza un back-end de acceso a archivos basado en
java.nio
. Este back-end asignaMappedByteBuffers
a los archivos en sí. - una vez cada cinco minutos, un subproceso del demonio JRobin llama a
MappedByteBuffer.force()
en cada base de datos subyacente de JRobin MBB
- utiliza un back-end de acceso a archivos basado en
-
pmap
listado:- 6500 mapeos
- 5500 de los cuales eran archivos de base de datos JRobin de 1.3MB, que funcionan a ~ 7.1GB
¡Ese último punto fue mi "Eureka"! momento.
Mis acciones correctivas:
- Considere actualizar al último JRobinLite 1.5.2 que aparentemente es mejor
- Implementar el manejo adecuado de los recursos en las bases de datos JRobin. Por el momento, una vez que mi aplicación crea una base de datos y luego nunca la vuelca después de que la base de datos ya no se usa activamente.
- Experimente moviendo el
MappedByteBuffer.force()
a eventos de actualización de base de datos, y no a un temporizador periódico. ¿El problema desaparecerá mágicamente? - Inmediatamente , cambie el back-end de JRobin a la implementación de java.io: un cambio de línea. Esto será más lento, pero posiblemente no sea un problema. Aquí hay un gráfico que muestra el impacto inmediato de este cambio.
Preguntas que puedo o no tener tiempo de averiguar:
- ¿Qué está pasando dentro de la JVM con
MappedByteBuffer.force()
? Si nada ha cambiado, ¿todavía escribe el archivo completo? Parte del archivo? ¿Lo carga primero? - ¿Hay una cierta cantidad de MBB siempre en RSS en todo momento? (RSS fue aproximadamente la mitad de los tamaños MBB asignados totales. ¿Coincidencia? Sospecho que no).
- Si muevo el
MappedByteBuffer.force()
a los eventos de actualización de la base de datos, y no a un temporizador periódico, ¿desaparecerá el problema mágicamente? - ¿Por qué la pendiente de RSS era tan regular? No se correlaciona con ninguna de las métricas de carga de la aplicación.
¿Por qué está pasando esto? ¿Qué está pasando "debajo del capó"?
JVM usa más memoria que solo el montón. Por ejemplo, los métodos Java, las pilas de subprocesos y los identificadores nativos se asignan en memoria separada del montón, así como las estructuras de datos internas de JVM.
En su caso, las posibles causas de los problemas pueden ser: NIO (ya mencionado), JNI (ya mencionado), creación de hilos excesivos.
Acerca de JNI, usted escribió que la aplicación no estaba utilizando JNI, pero ... ¿Qué tipo de controlador JDBC está utilizando? ¿Podría ser un tipo 2 y gotear? Sin embargo, es muy poco probable ya que dijo que el uso de la base de datos era bajo.
Sobre la creación de hilos excesivos, cada hilo obtiene su propia pila, que puede ser bastante grande. El tamaño de la pila en realidad depende de la máquina virtual, el sistema operativo y la arquitectura, por ejemplo, para JRockit es 256K en Linux x64, no encontré la referencia en la documentación de Sun para la máquina virtual de Sun. Esto impacta directamente en la memoria de hilo (memoria de hilo = tamaño de pila de hilo * número de hilos). Y si crea y destruye muchos hilos, es probable que la memoria no se reutilice.
¿Qué puedo hacer para mantener el consumo de memoria real de la JVM bajo control?
Para ser honesto, cientos a pocos miles de hilos me parecen enormes. Dicho eso, si realmente necesitas tantos subprocesos, el tamaño de la pila de subprocesos se puede configurar a través de la opción -Xss
. Esto puede reducir el consumo de memoria. Pero no creo que esto resuelva todo el problema. Tiendo a pensar que hay una fuga en alguna parte cuando miro el gráfico de memoria real.
El recolector de basura actual en Java es bien conocido por no liberar memoria asignada, aunque la memoria ya no es necesaria. Sin embargo, es bastante extraño que su tamaño de RSS aumente a> 3 GB, aunque su tamaño de almacenamiento dinámico está limitado a 640 MB. ¿Está utilizando algún código nativo en su aplicación o tiene el paquete de optimización de rendimiento nativo para Tomcat habilitado? En ese caso, puede tener una pérdida de memoria nativa en su código o en Tomcat.
Con Java 6u14, Sun presentó el nuevo recolector de basura "Basura-Primero", que puede devolver la memoria al sistema operativo si ya no es necesario. Todavía está categorizado como experimental y no está habilitado de manera predeterminada, pero si es una opción viable para usted, trataría de actualizar a la versión más reciente de Java 6 y habilitar el nuevo recolector de basura con los argumentos de línea de comando "-XX: + UnlockExperimentalVMOptions - XX: + UseG1GC ". Podría resolver su problema.
RSS representa páginas que están en uso activamente: para Java, principalmente son los objetos en vivo en el montón y las estructuras de datos internas en la JVM. No hay mucho que pueda hacer para reducir su tamaño, excepto que use menos objetos o menos procesamiento.
En tu caso, no creo que sea un problema. El gráfico parece mostrar 3 megas consumidas, no 3 gigas mientras escribes en el texto. Eso es realmente pequeño y es poco probable que cause paginación.
Entonces, ¿qué más está sucediendo en su sistema? ¿Es una situación en la que tienes muchos servidores Tomcat, cada uno consume 3M de RSS? Está lanzando una gran cantidad de banderas de GC, ¿indican que el proceso pasa la mayor parte del tiempo en GC? ¿Tiene una base de datos ejecutándose en la misma máquina?
Editar en respuesta a los comentarios
Con respecto al tamaño RSS de 3M, sí, eso parecía demasiado bajo para un proceso de Tomcat (revisé mi casillero, y tengo uno en 89M que no ha estado activo por un tiempo). Sin embargo, no necesariamente espero que sea> tamaño de almacenamiento dinámico, y ciertamente no espero que sea casi 5 veces el tamaño de almacenamiento dinámico (usa -Xmx640) - en el peor de los casos debe ser de tamaño dinámico o algo por aplicación constante.
Lo que me hace sospechar tus números. Por lo tanto, en lugar de un gráfico a lo largo del tiempo, ejecute lo siguiente para obtener una instantánea (reemplace 7429 por el ID de proceso que esté usando):
ps -p 7429 -o pcpu,cutime,cstime,cmin_flt,cmaj_flt,rss,size,vsize
(Editar por Stu para que podamos tener resultados formateados a la solicitud anterior de información ps :)
[stu@server ~]$ ps -p 12720 -o pcpu,cutime,cstime,cmin_flt,cmaj_flt,rss,size,vsize
%CPU - - - - RSS SZ VSZ
28.8 - - - - 3262316 1333832 8725584
Editar para explicar estos números para la posteridad
RSS, como se señaló, es el tamaño del conjunto residente: las páginas en la memoria física. SZ contiene el número de páginas que se pueden escribir en el proceso (la carga de confirmación); la página de manual describe este valor como "muy difícil". VSZ tiene el tamaño del mapa de memoria virtual para el proceso: páginas modificables más páginas compartidas.
Normalmente, VSZ es levemente> SZ, y mucho> RSS. Este resultado indica una situación muy inusual.
Elaboración de por qué la única solución es reducir objetos
RSS representa el número de páginas residentes en RAM: las páginas a las que se accede activamente. Con Java, el recolector de basura recorrerá periódicamente todo el gráfico de objetos. Si este gráfico de objetos ocupa la mayor parte del espacio de almacenamiento dinámico, el recopilador tocará todas las páginas en el montón, requiriendo que todas esas páginas se vuelvan residentes de memoria. El GC es muy bueno para compactar el montón después de cada colección principal, por lo que si está ejecutando un montón parcial, la mayoría de las páginas no deberían estar en la memoria RAM.
Y algunas otras opciones
Noté que mencionaste tener cientos a pocos miles de hilos. Los stacks de estos hilos también se agregarán al RSS, aunque no debería ser mucho. Suponiendo que los subprocesos tienen una profundidad de llamada superficial (típica para los subprocesos de controlador de servidor de aplicación), cada uno solo debería consumir una o dos páginas de memoria física, aunque hay un cargo de compromiso de medio mego por cada uno.
Solo una idea: los búferes de NIO se colocan fuera de la JVM.
EDITAR: Según 2016, vale la pena considerar el comentario de @Lari Hotari [ ¿Por qué Sun JVM sigue consumiendo cada vez más memoria RSS incluso cuando los tamaños de pila, etc. son estables? ] porque hasta 2009, RHEL4 tenía glibc <2.10 (~ 2.3)
Saludos.