limpiar - Uso de memoria virtual desde Java en Linux, demasiada memoria utilizada
memoria virtual linux mint (8)
Tengo un problema con una aplicación Java que se ejecuta en Linux.
Cuando ejecuto la aplicación, usando el tamaño de pila máximo predeterminado (64 MB), veo que al usar la parte superior de la aplicación se asignan 240 MB de memoria virtual. Esto crea algunos problemas con algún otro software en la computadora, que es relativamente limitado en recursos.
La memoria virtual reservada no se utilizará de todos modos, por lo que yo entiendo, porque una vez que alcanzamos el límite de OutOfMemoryError
se lanza un OutOfMemoryError
. Ejecuté la misma aplicación en Windows y veo que el tamaño de la memoria virtual y el tamaño del montón son similares.
¿Hay alguna forma en que pueda configurar la memoria virtual en uso para un proceso Java en Linux?
Edición 1 : El problema no es el montón. El problema es que si configuro un montón de 128 MB, por ejemplo, aún Linux asigna 210 MB de memoria virtual, que no es necesario, nunca. **
Edición 2 : el uso de ulimit -v
permite limitar la cantidad de memoria virtual. Si el tamaño del conjunto es inferior a 204 MB, la aplicación no se ejecutará aunque no necesite 204 MB, solo 64 MB. Así que quiero entender por qué Java requiere tanta memoria virtual. ¿Se puede cambiar esto?
Edición 3 : hay otras aplicaciones que se ejecutan en el sistema, que está incrustado. Y el sistema tiene un límite de memoria virtual (a partir de comentarios, detalles importantes).
Esta ha sido una queja de larga data con Java, pero en gran medida no tiene sentido, y generalmente se basa en mirar la información incorrecta. El fraseo habitual es algo así como "¡Hello World en Java toma 10 megabytes! ¿Por qué necesita eso?" Bueno, aquí hay una manera de hacer que Hello World en un reclamo JVM de 64 bits tome más de 4 gigabytes ... al menos por una forma de medición.
java -Xms1024m -Xmx4096m com.example.Hello
Diferentes maneras de medir la memoria
En Linux, el comando top le da varios números diferentes para la memoria. Esto es lo que dice sobre el ejemplo de Hello World:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2120 kgregory 20 0 4373m 15m 7152 S 0 0.2 0:00.10 java
- VIRT es el espacio de memoria virtual: la suma de todo en el mapa de memoria virtual (ver más abajo). Es en gran medida sin sentido, excepto cuando no lo es (ver más abajo).
- RES es el tamaño del conjunto residente: la cantidad de páginas que actualmente residen en la RAM. En casi todos los casos, este es el único número que debe usar cuando dice "demasiado grande". Pero todavía no es un número muy bueno, especialmente cuando se habla de Java.
- SHR es la cantidad de memoria residente que se comparte con otros procesos. Para un proceso Java, esto generalmente se limita a bibliotecas compartidas y archivos JAR asignados en memoria. En este ejemplo, solo tuve un proceso Java en ejecución, por lo que sospecho que 7k es el resultado de las bibliotecas utilizadas por el sistema operativo.
- SWAP no está activado de forma predeterminada, y no se muestra aquí. Indica la cantidad de memoria virtual que reside actualmente en el disco, ya sea que esté o no en el espacio de intercambio . El sistema operativo es muy bueno para mantener las páginas activas en la RAM, y las únicas soluciones para el intercambio son (1) comprar más memoria, o (2) reducir el número de procesos, por lo que es mejor ignorar este número.
La situación para Windows Task Manager es un poco más complicada. Bajo Windows XP, hay columnas de "Uso de la memoria" y "Tamaño de la memoria virtual", pero la documentación oficial no dice nada sobre lo que significan. Windows Vista y Windows 7 agregan más columnas, y en realidad están documented . De estos, la medida del "Conjunto de trabajo" es la más útil; corresponde aproximadamente a la suma de RES y SHR en Linux.
Entendiendo el mapa de memoria virtual
La memoria virtual consumida por un proceso es el total de todo lo que hay en el mapa de memoria de proceso. Esto incluye datos (por ejemplo, el montón de Java), pero también todas las bibliotecas compartidas y archivos asignados en memoria utilizados por el programa. En Linux, puede usar el comando pmap para ver todas las cosas asignadas en el espacio de proceso (de aquí en adelante solo voy a referirme a Linux, porque es lo que uso; estoy seguro de que existen herramientas equivalentes para Windows). Aquí hay un extracto del mapa de memoria del programa "Hello World"; El mapa de memoria completo tiene más de 100 líneas de largo, y no es raro tener una lista de mil líneas.
0000000040000000 36K r-x-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java 0000000040eba000 676K rwx-- [ anon ] 00000006fae00000 21248K rwx-- [ anon ] 00000006fc2c0000 62720K rwx-- [ anon ] 0000000700000000 699072K rwx-- [ anon ] 000000072aab0000 2097152K rwx-- [ anon ] 00000007aaab0000 349504K rwx-- [ anon ] 00000007c0000000 1048576K rwx-- [ anon ] ... 00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar ... 00007fa1ed1d3000 1024K rwx-- [ anon ] 00007fa1ed2d3000 4K ----- [ anon ] 00007fa1ed2d4000 1024K rwx-- [ anon ] 00007fa1ed3d4000 4K ----- [ anon ] ... 00007fa1f20d3000 164K r-x-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f20fc000 1020K ----- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so 00007fa1f21fb000 28K rwx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so ... 00007fa1f34aa000 1576K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3833000 16K r-x-- /lib/x86_64-linux-gnu/libc-2.13.so 00007fa1f3837000 4K rwx-- /lib/x86_64-linux-gnu/libc-2.13.so ...
Una explicación rápida del formato: cada fila comienza con la dirección de memoria virtual del segmento. A esto le sigue el tamaño del segmento, los permisos y la fuente del segmento. Este último elemento es un archivo o "anon", que indica un bloque de memoria asignado a través de mmap .
A partir de la parte superior, tenemos
- El cargador JVM (es decir, el programa que se ejecuta cuando se escribe
java
). Esto es muy pequeño; todo lo que hace es cargar en las bibliotecas compartidas donde se almacena el código real de JVM. - Un montón de bloques anon que contienen el montón de Java y los datos internos. Esta es una Sun JVM, por lo que el montón se divide en varias generaciones, cada una de las cuales es su propio bloque de memoria. Tenga en cuenta que la JVM asigna espacio de memoria virtual basado en el valor
-Xmx
; esto le permite tener un montón contiguo. El valor de-Xms
se usa internamente para-Xms
qué parte del montón está "en uso" cuando se inicia el programa, y para activar la recolección de basura cuando se aproxima ese límite. - Un archivo JAR asignado en memoria, en este caso el archivo que contiene las "clases JDK". Cuando asigna en memoria un JAR, puede acceder a los archivos dentro de él de manera muy eficiente (en lugar de leerlo desde el principio cada vez). Sun JVM asignará en memoria todos los JAR en el classpath; Si su código de aplicación necesita acceder a un JAR, también puede mapearlo en la memoria.
- Datos por hilo para dos hilos. El bloque 1M es una pila de hilos; No sé qué pasa en el bloque 4K. Para una aplicación real, verá docenas, si no cientos de estas entradas repetidas a través del mapa de memoria.
- Una de las bibliotecas compartidas que contiene el código JVM real. Hay varios de estos.
- La biblioteca compartida para la biblioteca estándar de C. Esta es solo una de las muchas cosas que carga la JVM que no son estrictamente parte de Java.
Las bibliotecas compartidas son particularmente interesantes: cada biblioteca compartida tiene al menos dos segmentos: un segmento de solo lectura que contiene el código de la biblioteca y un segmento de lectura y escritura que contiene datos globales por proceso para la biblioteca (no sé qué segmento sin permisos es; solo lo he visto en x64 Linux). La parte de solo lectura de la biblioteca se puede compartir entre todos los procesos que la utilizan; por ejemplo, libc
tiene 1.5M de espacio de memoria virtual que se puede compartir.
¿Cuándo es importante el tamaño de la memoria virtual?
El mapa de memoria virtual contiene muchas cosas. Algunos de ellos son de solo lectura, otros se comparten y otros se asignan pero nunca se tocan (por ejemplo, casi todos los 4Gb de pila en este ejemplo). Pero el sistema operativo es lo suficientemente inteligente como para cargar solo lo que necesita, por lo que el tamaño de la memoria virtual es en gran medida irrelevante.
El tamaño de la memoria virtual es importante si está ejecutando un sistema operativo de 32 bits, donde solo puede asignar 2 Gb (o, en algunos casos, 3Gb) de espacio de direcciones de proceso. En ese caso, se trata de un recurso escaso y es posible que tenga que hacer concesiones, como reducir el tamaño del montón para mapear en memoria un archivo grande o crear muchos subprocesos.
Pero, dado que las máquinas de 64 bits son ubicuas, no creo que pase mucho tiempo antes de que el tamaño de la memoria virtual sea una estadística completamente irrelevante.
¿Cuándo es importante el tamaño del conjunto residente?
El tamaño del conjunto residente es la parte del espacio de memoria virtual que está realmente en la RAM. Si su RSS se convierte en una parte importante de su memoria física total, puede ser el momento de comenzar a preocuparse. Si su RSS crece para ocupar toda su memoria física, y su sistema comienza a intercambiarse, ya es hora de empezar a preocuparse.
Pero el RSS también es engañoso, especialmente en una máquina con poca carga. El sistema operativo no gasta mucho esfuerzo para reclamar las páginas utilizadas por un proceso. Hay pocos beneficios que se pueden obtener al hacerlo, y el potencial de un error de página costoso si el proceso toca la página en el futuro. Como resultado, la estadística RSS puede incluir muchas páginas que no están en uso activo.
Línea de fondo
A menos que esté intercambiando, no se preocupe demasiado por lo que le dicen las distintas estadísticas de memoria. Con la advertencia de que un RSS en constante crecimiento puede indicar algún tipo de pérdida de memoria.
Con un programa Java, es mucho más importante prestar atención a lo que sucede en el montón. La cantidad total de espacio consumido es importante, y hay algunos pasos que puede tomar para reducirlo. Más importante es la cantidad de tiempo que pasa en la recolección de basura, y qué partes del montón se están recolectando.
El acceso al disco (es decir, una base de datos) es costoso y la memoria es barata. Si puedes intercambiar uno por el otro, hazlo.
Hay un problema conocido con Java y glibc> = 2.10 (incluye Ubuntu> = 10.04, RHEL> = 6).
La cura es configurar esta env. variable: export MALLOC_ARENA_MAX=4
Si está ejecutando Tomcat, puede agregarlo al TOMCAT_HOME/bin/setenv.sh
.
Hay un artículo de IBM sobre la configuración de MALLOC_ARENA_MAX https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en
Se sabe que la memoria residente se arrastra de una manera similar a una pérdida de memoria o fragmentación de la memoria.
busque MALLOC_ARENA_MAX en Google o SO para obtener más referencias.
Es posible que desee ajustar también otras opciones de malloc para optimizar la baja fragmentación de la memoria asignada:
# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536
La cantidad de memoria asignada para el proceso de Java está prácticamente a la par con lo que yo esperaría. He tenido problemas similares al ejecutar Java en sistemas integrados / con memoria limitada. La ejecución de cualquier aplicación con límites de VM arbitrarios o en sistemas que no tienen cantidades adecuadas de swap tiende a romperse. Parece ser la naturaleza de muchas aplicaciones modernas que no están diseñadas para su uso en sistemas con recursos limitados.
Tiene algunas opciones más que puede intentar y limitar la huella de memoria de su JVM. Esto podría reducir la huella de memoria virtual:
-XX: ReservedCodeCacheSize = 32m Tamaño de caché de código reservado (en bytes) - Tamaño máximo de caché de código. [Solaris de 64 bits, amd64 y servidor x86: 48m; en 1.5.0_06 y anteriores, Solaris de 64 bits y and64: 1024m.]
-XX: MaxPermSize = 64m Tamaño de la generación permanente. [5.0 y más reciente: las máquinas virtuales de 64 bits se escalan un 30% más; 1,4 amd64: 96 m; 1.3.1 -cliente: 32m.]
Además, también debe establecer su -Xmx (tamaño máximo de pila) en un valor lo más cercano posible al uso de memoria máximo real de su aplicación. Creo que el comportamiento predeterminado de la JVM sigue siendo duplicar el tamaño del montón cada vez que se expande hasta el máximo. Si comienzas con un montón de 32M y tu aplicación alcanzó un máximo de 65M, entonces el montón terminaría creciendo 32M -> 64M -> 128M.
También puedes probar esto para hacer que la máquina virtual sea menos agresiva al hacer crecer el montón:
-XX: MinHeapFreeRatio = 40 Porcentaje mínimo de almacenamiento dinámico libre después de GC para evitar la expansión.
Además, de lo que recuerdo haber experimentado con esto hace unos años, la cantidad de bibliotecas nativas cargadas tuvo un gran impacto en la huella mínima. Cargando java.net.Socket agregó más de 15M si recuerdo correctamente (y probablemente no lo haga).
No, no puede configurar la cantidad de memoria que necesita la máquina virtual. Sin embargo, tenga en cuenta que esta es una memoria virtual, no residente, por lo que solo permanece allí sin daño si no se utiliza realmente.
Como alternativa, puede probar con otra JVM y luego con Sun uno, con una huella de memoria más pequeña, pero no puedo recomendar aquí.
Solo un pensamiento, pero puede comprobar la influencia de una opción ulimit -v
.
Esa no es una solución real ya que limitaría el espacio de direcciones disponible para todos los procesos, pero le permitiría verificar el comportamiento de su aplicación con una memoria virtual limitada.
Sun JVM requiere una gran cantidad de memoria para HotSpot y se asigna en las bibliotecas de tiempo de ejecución en la memoria compartida.
Si la memoria es un problema, considere usar otra JVM adecuada para incrustar. IBM tiene j9, y existe el código abierto "jamvm" que utiliza las bibliotecas de ruta de clase de GNU. Además, Sun tiene la JVM Squeak corriendo en los SunSPOTS, por lo que hay alternativas.
Sun Java 1.4 tiene los siguientes argumentos para controlar el tamaño de la memoria:
-Xmsn Especifique el tamaño inicial, en bytes, del grupo de asignación de memoria. Este valor debe ser un múltiplo de 1024 mayor que 1MB. Agregue la letra ko K para indicar kilobytes, o m o M para indicar megabytes. El valor predeterminado es 2MB. Ejemplos:
-Xms6291456 -Xms6144k -Xms6m
-Xmxn Especifique el tamaño máximo, en bytes, del grupo de asignación de memoria. Este valor debe ser un múltiplo de 1024 mayor que 2MB. Agregue la letra ko K para indicar kilobytes, o m o M para indicar megabytes. El valor predeterminado es 64MB. Ejemplos:
-Xmx83886080 -Xmx81920k -Xmx80m
http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/java.html
Java 5 y 6 tienen algo más. Consulte http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp
Una forma de reducir el tamaño de un sistema con recursos limitados puede ser jugar con la variable -XX: MaxHeapFreeRatio. Esto generalmente se establece en 70 y es el porcentaje máximo del montón que está libre antes de que el GC lo reduzca. Estableciéndolo en un valor más bajo, y verá, por ejemplo, en el analizador jvisualvm que usualmente se usa un tamaño de pila más pequeño para su programa.
EDITAR: Para establecer valores pequeños para -XX: MaxHeapFreeRatio también debe establecer -XX: MinHeapFreeRatio Eg
java -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=25 HelloWorld
EDIT2: Se agregó un ejemplo para una aplicación real que se inicia y realiza la misma tarea, una con parámetros predeterminados y otra con 10 y 25 como parámetros. No noté ninguna diferencia real de velocidad, aunque Java en teoría debería usar más tiempo para aumentar el montón en el último ejemplo.
Al final, el montón máximo es 905, el montón usado es 378
Al final, el montón máximo es 722, el montón usado es 378
Esto realmente tiene algún impacto, ya que nuestra aplicación se ejecuta en un servidor de escritorio remoto, y muchos usuarios pueden ejecutarla a la vez.