Uso de memoria virtual de Java bajo Linux, demasiada memoria utilizada

259

Tengo un problema con una aplicación Java que se ejecuta en Linux.

Cuando inicio la aplicación, usando el tamaño de almacenamiento dinámico máximo predeterminado (64 MB), veo usando la aplicación superior que 240 MB de memoria virtual se asignan a la aplicación. 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 entiendo, porque una vez que alcanzamos el límite de almacenamiento dinámico, OutOfMemoryErrorse arroja una. 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 de 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, Linux aún asigna 210 MB de memoria virtual, que nunca es necesaria. **

Edición 2 : el uso ulimit -vpermite limitar la cantidad de memoria virtual. Si el tamaño establecido 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 varias otras aplicaciones ejecutándose en el sistema, que está incrustado. Y el sistema tiene un límite de memoria virtual (de comentarios, detalles importantes).

Mario Ortegón
fuente
¿Por qué le preocupa el uso de memoria virtual? Si realmente quiere preocuparse, observe el uso de la memoria residente y lea los siguientes comandos: free, ps, top.
basszero
2
Hay varias otras aplicaciones ejecutándose en el sistema, que está integrado. Y el sistema tiene un límite de memoria virtual.
Mario Ortegón
ahhhh, diablo está en los detalles
basszero
¿Qué implementación de Java estás usando? IIRC, el Sun BRE libre de estándares (no OpenJDK) no tiene licencia para uso incorporado.
Tom Hawtin - tackline
Creo que miss-utilicé la parte "incrustado" ... es limitado memoria y el hardware es personalizado, pero sigue siendo un ordenador estándar
Mario Ortegón

Respuestas:

630

Esta ha sido una queja de larga data con Java, pero en gran medida no tiene sentido, y generalmente se basa en buscar información incorrecta. La redacción 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 JVM de 64 bits reclame más de 4 gigabytes ... al menos por una forma de medición.

java -Xms1024m -Xmx4096m com.example.Hola

Diferentes formas de medir la memoria

En Linux, el comando superior le proporciona varios números diferentes para la memoria. Esto es lo que dice sobre el ejemplo de Hello World:

  PID USUARIO PR NI VIRT RES SHR S% CPU% MEM TIME + COMMAND
 2120 kg registro 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 parte sin sentido, excepto cuando no lo es (ver más abajo).
  • RES es el tamaño del conjunto residente: el número de páginas que actualmente residen en la RAM. En casi todos los casos, este es el único número que debe usar al decir "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 normalmente se limita a bibliotecas compartidas y archivos JAR asignados a memoria. En este ejemplo, solo tenía un proceso Java en ejecución, por lo que sospecho que el 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 actualmente reside 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 curas 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 del Administrador de tareas de Windows es un poco más complicada. En Windows XP, hay columnas de "Uso de memoria" y "Tamaño de memoria virtual", pero la documentación oficial no menciona qué significan. Windows Vista y Windows 7 agregan más columnas, y en realidad están documentadas . De estos, la medición del "conjunto de trabajo" es la más útil; corresponde aproximadamente a la suma de RES y SHR en Linux.

Comprensión del mapa de memoria virtual

La memoria virtual consumida por un proceso es el total de todo lo que está en el mapa de memoria del proceso. Esto incluye datos (por ejemplo, el montón de Java), pero también todas las bibliotecas compartidas y los archivos asignados a la memoria utilizados por el programa. En Linux, puede usar el comando pmap para ver todas las cosas asignadas al espacio de proceso (de ahora en adelante solo me referiré a Linux, porque es lo que uso; estoy seguro de que hay herramientas equivalentes para Windows) Aquí hay un extracto del mapa de memoria del programa "Hello World"; todo el mapa de memoria tiene más de 100 líneas de largo, y no es inusual tener una lista de mil líneas.

0000000040000000 36K rx-- /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 rx-- /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 rx-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000 16K rx-- /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. Esto es seguido por 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 .

Comenzando desde arriba, tenemos

  • El cargador JVM (es decir, el programa que se ejecuta cuando escribe java). Esto es muy pequeño; todo lo que hace es cargar en las bibliotecas compartidas donde se almacena el código JVM real.
  • Un montón de bloques anon que contienen el montón de Java y los datos internos. Este es un 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 en función del -Xmxvalor; Esto le permite tener un montón contiguo. El -Xmsvalor se usa internamente para decir qué cantidad del montón está "en uso" cuando se inicia el programa, y ​​para activar la recolección de basura a medida que se acerca a ese límite.
  • Un archivo JAR mapeado en memoria, en este caso el archivo que contiene las "clases JDK". Cuando asigna un JAR en memoria, puede acceder a los archivos dentro de él de manera muy eficiente (en lugar de leerlo desde el principio cada vez). Sun JVM hará un mapa de memoria de todos los JAR en el classpath; Si el código de su aplicación necesita acceder a un JAR, también puede asignarlo en la memoria.
  • Datos por hilo para dos hilos. El bloque 1M es la pila de hilos. No tenía una buena explicación para el bloque de 4k, pero @ericsoe lo identificó como un "bloque de protección": no tiene permisos de lectura / escritura, por lo que causará un fallo de segmento si se accede, y la JVM lo detecta y traduce a un StackOverFlowError. 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 C. Esta es solo una de las muchas cosas que carga 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é cuál es el segmento sin permisos; solo lo he visto en Linux x64). La parte de solo lectura de la biblioteca se puede compartir entre todos los procesos que la utilizan; por ejemplo, libctiene 1,5 millones 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 4 Gb de almacenamiento dinámico 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.

Donde el tamaño de la memoria virtual es importante es si está ejecutando en un sistema operativo de 32 bits, donde solo puede asignar 2Gb (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 almacenamiento dinámico para asignar un archivo grande en la memoria o crear muchos subprocesos.

Pero, dado que las máquinas de 64 bits son ubicuas, no creo que pasará mucho tiempo antes de que Virtual Memory Size 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, podría ser hora de comenzar a preocuparse. Si su RSS crece para ocupar toda su memoria física y su sistema comienza a intercambiarse, es hora de que empiece a preocuparse.

Pero RSS también es engañoso, especialmente en una máquina con poca carga. El sistema operativo no gasta mucho esfuerzo para recuperar las páginas utilizadas por un proceso. Al hacerlo, se obtienen pocos beneficios y existe la posibilidad de un costoso error de página 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 diversas estadísticas de memoria. Con la advertencia de que un RSS cada vez mayor 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 seguir para reducir eso. 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.

Acceder al disco (es decir, una base de datos) es costoso y la memoria es barata. Si puede cambiar uno por el otro, hágalo.

kdgregory
fuente
9
Debe tener en cuenta que las porciones de memoria que se intercambian actualmente faltan en la medida RES. Por lo tanto, es posible que tenga un valor RES bajo, pero solo porque la aplicación estaba inactiva y gran parte del montón se cambió al disco. Java hace un trabajo muy malo para intercambiar: en cada GC completo, la mayor parte del montón se recorre y copia, por lo que si gran parte de su montón estaba en intercambio, el GC tiene que cargarlo todo de nuevo en la memoria principal.
jrudolph
1
Gran respuesta kdgregory! Estoy corriendo en un entorno incrustado usando un CF que NO tiene espacio de intercambio. Entonces, según su respuesta, todos mis valores de VIRT, SWAP y nFLT provienen de archivos mapeados en memoria ... lo que ahora tiene sentido para maullar. ¿Sabe si el valor de SWAP representa páginas que aún no se han cargado en la memoria o páginas que se han cambiado de memoria, o ambas? ¿Cómo podemos tener una idea de una posible paliza (mapa continuo y luego cambiar)?
Jeach
2
@Jeach: me sorprendió que se informara de cualquier intercambio, así que arranqué mi "Linux itinerante" (una unidad de memoria USB con Ubuntu 10.04 y sin intercambio). Cuando habilité la columna "SWAP" en la parte superior , vi que Eclipse tenía 509 m. Cuando lo miré con pmap , el espacio virtual total era de 650 m. Entonces sospecho que la figura "SWAP" representa todas las páginas en el disco, no solo aquellas que no están en la memoria.
kdgregory
2
En cuanto a su segunda pregunta: si está leyendo constantemente páginas de la tarjeta flash, su tiempo de espera IO (que se muestra en el resumen de arriba como "% wa") debe ser alto. Sin embargo, tenga en cuenta que esto será alto para cualquier actividad, especialmente las escrituras (suponiendo que su programa haga alguna).
kdgregory
1
> El bloque 1M es una pila de hilos; No sé qué entra en el bloque 4K. El bloque 4K, que está marcado como que no tiene permisos de lectura ni escritura, es probable que sea un bloque de protección. En el desbordamiento de la pila, se accede a esta área, que desencadena una falla, que la JVM puede manejar generando una excepción StackOverflowException de Java. Esto es mucho más barato que verificar el puntero de la pila en cada llamada al método. Las áreas de protección sin permisos establecidos también se pueden ver utilizadas en otros contextos.
eriksoe
38

Hay un problema conocido con Java y glibc> = 2.10 (incluye Ubuntu> = 10.04, RHEL> = 6).

La cura es establecer este ambiente. variable:

export MALLOC_ARENA_MAX=4

Si está ejecutando Tomcat, puede agregar esto al TOMCAT_HOME/bin/setenv.sharchivo.

Para Docker, agregue esto a Dockerfile

ENV MALLOC_ARENA_MAX=4

Hay un artículo de IBM sobre cómo configurar 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

Esta publicación de blog dice

Se sabe que la memoria residente se arrastra de manera similar a una pérdida de memoria o fragmentación de memoria.

También hay un error JDK abierto JDK -8193521 "glibc desperdicia memoria con la configuración predeterminada"

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
Lari Hotari
fuente
Esta respuesta realmente me ayudó en un servidor Ubuntu de 64 bits con un servidor TomEE que tuvo un poco de "consumo de memoria". El enlace al artículo de IBM realmente es una explicación profunda. Gracias de nuevo por esta buena pista!
MWiesner
1
La JVM podría perder memoria nativa, lo que lleva a síntomas similares. Ver stackoverflow.com/a/35610063/166062 . Las instancias GZIPInputStream y GZIPOutputStream no cerradas también podrían ser una fuente de la fuga.
Lari Hotari
3
Hay un error de JVM en Java 8, que produce un crecimiento ilimitado de la memoria nativa: bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8164293 : si esto le afecta, el uso MALLOC_ARENA_MAXpuede ralentizar el crecimiento de la memoria, pero no resuelve el problema por completo.
outofcoffee
@LariHotari realmente aprecia su esfuerzo por señalar la versión glibc y redhat
Sam
2
Java 8u131 contiene correcciones de errores con respaldo para el error relacionado JVM JDK-8164293 bugs.openjdk.java.net/browse/JDK-8178124 .
Lari Hotari
9

La cantidad de memoria asignada para el proceso de Java está bastante a la par con lo que 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 intercambio 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 probar 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 -server 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: 96m; 1.3.1 -cliente: 32m.]

Además, también debe establecer su -Xmx (tamaño máximo de almacenamiento dinámico) en un valor lo más cercano posible al uso de memoria pico real de su aplicación. Creo que el comportamiento predeterminado de la JVM sigue siendo duplicar el tamaño del almacenamiento dinámico cada vez que lo expande al máximo. Si comienza con un montón de 32M y su aplicación alcanzó un máximo de 65M, entonces el montón terminaría creciendo 32M -> 64M -> 128M.

También puede intentar esto para que la máquina virtual sea menos agresiva sobre el crecimiento del montón:

-XX: MinHeapFreeRatio = 40 Porcentaje mínimo de montón libre después de GC para evitar la expansión.

Además, por lo que recuerdo de experimentar 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 15 millones si recuerdo correctamente (y probablemente no).

James Schek
fuente
7

Sun JVM requiere mucha 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 bibliotecas de classpath de GNU. También Sun tiene el Squeak JVM ejecutándose en SunSPOTS, por lo que existen alternativas.

Thorbjørn Ravn Andersen
fuente
¿Es una opción deshabilitar hot spot?
Mario Ortegón el
Quizás. Verifique las opciones de línea de comando para la JVM que utiliza.
Thorbjørn Ravn Andersen
3

Solo un pensamiento, pero puede comprobar la influencia de una ulimit -vopción .

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.

VonC
fuente
Ese es exactamente mi problema. My Heap está configurado en 64M, pero Linux reserva 204MB. Si configuro el ulimit por debajo de 204, la aplicación no se ejecuta en absoluto.
Mario Ortegón
Interesante: configurar el ulimit podría tener efectos secundarios no deseados para otros procesos, explicando por qué la aplicación no puede ejecutarse.
VonC
El problema parece ser que Java requiere reservar esta mayor cantidad de memoria virtual a pesar de que no la usará. En Windows, la memoria virtual utilizada y la configuración Xmx están bastante más cerca.
Mario Ortegón
¿Lo probaste con un JRockit JVM?
VonC
Dado que la asignación de memoria de la JVM es la suma de la asignación de almacenamiento dinámico y del tamaño de Perm (la primera se puede arreglar con las opciones -Xms y -Xmx), ¿intentó algunas configuraciones con -XX: PermSize y -XX: MaxPermSize (predeterminado de 32 MB a 64 MB dependiendo de la versión JVM)?
VonC
3

Una forma de reducir el almacenamiento dinámico 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. Si lo configura en un valor más bajo, verá en, por ejemplo, el analizador de jvisualvm, que generalmente se usa un montón 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 de velocidad real, aunque en teoría Java debería usar más tiempo para aumentar el montón en el último ejemplo.

Parámetros por defecto

Al final, el montón máximo es 905, el montón usado es 378

MinHeap 10, MaxHeap 25

Al final, el montón máximo es 722, el montón usado es 378

En realidad, esto tiene cierto impacto, ya que nuestra aplicación se ejecuta en un servidor de escritorio remoto, y muchos usuarios pueden ejecutarla a la vez.

runholen
fuente
1

Sun's 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 1 MB. Agregue la letra k o K para indicar kilobytes, o mo M para indicar megabytes. El valor predeterminado es 2 MB. Ejemplos:

           -Xms6291456
           -Xms6144k
           -Xms6m

-Xmxn Especifica 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 2 MB. Agregue la letra k o K para indicar kilobytes, o mo M para indicar megabytes. El valor predeterminado es 64 MB. 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. Ver http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp

Paul Tomblin
fuente
1
El problema que tengo no es con el tamaño del montón, sino con la cantidad de memoria virtual que asigna Linux
Mario Ortegón
Lea la explicación de kdgregory. La reducción del tamaño de almacenamiento dinámico, "Nuevo tamaño" y los otros parámetros configurables reducirán la cantidad de memoria REAL que toma jvm.
Paul Tomblin
Él puede tener un problema legítimo. Algunas aplicaciones (como una que escribí) mmap un archivo de 1 GB y algunos sistemas solo tienen 2 GB de memoria virtual, algunos de los cuales se llenan de bibliotecas compartidas. Y si este es el problema, definitivamente debería deshabilitar la aleatorización DSO. Hay una opción en / proc.
Zan Lynx
0

No, no puede configurar la cantidad de memoria que necesita VM. Sin embargo, tenga en cuenta que esto es memoria virtual, no residente, por lo que simplemente permanece allí sin daños si no se usa realmente.

Alernativamente, puede probar alguna otra JVM y luego Sun one, con menor huella de memoria, pero no puedo aconsejar aquí.

Marko
fuente