Pregunta ¿Por qué Sun JVM continúa consumiendo memoria RSS aún más cuando los tamaños de pila, etc. son estables?


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.

Tamaño del montón medido por Java SNMP Gráfico usado Java Heap 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? Que esta pasando "bajo el 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óns:

Mientras que al final fue ATorrasLa respuesta que resultó finalmente correcta, kdgregory que 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:

  1. 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.
  2. La instancia más activa de la aplicación actualmente crea
    1. Más de 1000 nuevos archivos de base de datos de JRobin (aproximadamente 1,3 MB cada uno) una hora después de iniciarse
    2. ~ 100 + cada día después de la puesta en marcha
  3. La aplicación actualiza estos objetos de la base de datos JRobin una vez cada 15 s, si hay algo que escribir.
  4. En la configuración predeterminada JRobin:
    1. usa un java.nioback-end de acceso a archivos basado en Estos mapas back-end MappedByteBuffersa los archivos mismos.
    2. una vez cada cinco minutos llama un hilo daemon JRobin MappedByteBuffer.force() en cada base de datos subyacente JRobin MBB
  5. pmap listado:
    1. 6500 mapeos
    2. 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:

  1. Considere actualizar al último JRobinLite 1.5.2 que aparentemente es mejor
  2. 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.
  3. 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?
  4. 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.

La memoria RSS de Java utilizó el gráfico http://lanai.dietpizza.ch/images/stackoverflow-rss-problem-fixed.png

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.

32
2017-10-23 11:50


origen


Respuestas:


Solo una idea: los búferes de NIO se colocan fuera de la JVM.

EDITAR: Según 2016, vale la pena considerar @Lari Hotari comment [ ¿Por qué Sun JVM continúa consumiendo memoria RSS aún más cuando los tamaños de pila, etc. son estables? ] porque hasta 2009, RHEL4 tenía glibc <2.10 (~ 2.3)

Saludos.


18
2017-10-23 12:19



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.


14
2017-10-23 12:01



¿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 realmente depende de la máquina virtual, el sistema operativo y la arquitectura, p. para JRockit es 256K en Linux x64, no encontré la referencia en la documentación de Sun para VM 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 esto, si realmente necesita tantos subprocesos, el tamaño de la pila de subprocesos se puede configurar a través del -Xss opción. 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.


3
2017-10-23 14:35



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.


1
2017-10-23 12:19