GNU/Linux >> Tutoriales Linux >  >> Linux

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

Esta ha sido una queja de larga data con Java, pero en gran medida no tiene sentido y, por lo general, se basa en buscar información incorrecta. La redacción habitual es algo así como "¡Hello World en Java ocupa 10 megabytes! ¿Por qué necesita eso?" Bueno, aquí hay una manera de hacer que Hello World en una JVM de 64 bits afirme ocupar 4 gigabytes... al menos según una forma de medición.

java -Xms1024m -Xmx4096m com.example.Hello

Diferentes formas de medir la memoria

En Linux, el comando superior le brinda 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). En gran parte no tiene sentido, excepto cuando no lo es (ver más abajo).
  • RES es el tamaño del conjunto residente:la cantidad de páginas que residen actualmente 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 de Java, esto generalmente se limita a bibliotecas compartidas y archivos JAR asignados a la memoria. En este ejemplo, solo tenía un proceso Java ejecutándose, por lo que sospecho que los 7k son 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 curas para el intercambio son (1) comprar más memoria o (2) reducir la cantidad de procesos, por lo que es mejor ignorar esta cantidad.

La situación para el 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 dice nada sobre lo que significan. Windows Vista y Windows 7 agregan más columnas y, de hecho, están documentadas. De estos, la medida del "Conjunto de Trabajo" es la más útil; corresponde aproximadamente a la suma de RES y SHR en Linux.

Comprender el 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 (p. ej., 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 mapeadas en el espacio del proceso (de ahora en adelante solo me referiré a Linux, porque es lo que uso; estoy seguro de que hay herramientas equivalentes para ventanas). He aquí un extracto del mapa de memoria del programa "Hello World"; el mapa de memoria completo tiene más de 100 líneas y no es inusual 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.

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 grupo de bloques Anon que contienen el montón de Java y los datos internos. Esta es una JVM de Sun, 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 -Xmx valor; esto le permite tener un montón contiguo. El -Xms El valor se usa internamente para decir cuánto del montón está "en uso" cuando se inicia el programa y para activar la recolección de elementos no utilizados 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 mapeas en memoria un JAR, puedes acceder a los archivos dentro de él de manera muy eficiente (en lugar de leerlo desde el principio cada vez). Sun JVM mapeará en memoria todos los JAR en el classpath; si el código de su aplicación necesita acceder a un JAR, también puede mapearlo en memoria.
  • Datos por subproceso para dos subprocesos. El bloque 1M es la pila de subprocesos. No tenía una buena explicación para el bloque 4k, pero @ericsoe lo identificó como un "bloque de protección":no tiene permisos de lectura/escritura, por lo que causará una falla de segmento si se accede, y la JVM lo detecta y lo 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 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é cuál es el 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 usan la biblioteca; por ejemplo, libc tiene 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. Una parte es de solo lectura, otra parte se comparte y otra parte se asigna pero nunca se toca (p. ej., 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 un sistema operativo de 32 bits, donde solo puede asignar 2 Gb (o, en algunos casos, 3 Gb) 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 en memoria un archivo grande o crear muchos subprocesos.

Pero, dado que las máquinas de 64 bits son omnipresentes, 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 de residentes?

El tamaño del conjunto residente es la parte del espacio de memoria virtual que se encuentra realmente en la RAM. Si su RSS crece hasta convertirse en una parte significativa de su memoria física total, puede que sea el momento de empezar a preocuparse. Si su RSS crece hasta ocupar toda su memoria física y su sistema comienza a cambiar, ya 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 dedica mucho esfuerzo a recuperar las páginas utilizadas por un proceso. Se obtienen pocos beneficios al hacerlo, y existe la posibilidad de que se produzca una falla de página costosa si el proceso toca la página en el futuro. Como resultado, la estadística de RSS puede incluir muchas páginas que no están en uso activo.

Conclusión

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 reducirlo. Más importante es la cantidad de tiempo que dedica a la recolección de basura y qué partes del montón se recolectan.

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


La cantidad de memoria asignada para el proceso de Java está bastante a la par de lo que esperaría. He tenido problemas similares al ejecutar Java en sistemas integrados/de memoria limitada. Ejecutando cualquiera la aplicación con límites arbitrarios de VM o en sistemas que no tienen cantidades adecuadas de intercambio tienden a fallar. 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 servidor x86:48 m; in1.5.0_06 y anteriores, Solaris de 64 bits y and64:1024 m.]

-XX:MaxPermSize=64m Tamaño de la Generación Permanente. [5.0 y posteriores:las máquinas virtuales de 64 bits se escalan un 30 % más; 1.4amd64: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 máximo real de memoria de su aplicación. Creo que el comportamiento predeterminado de la JVM sigue siendo doble el tamaño del montón cada vez que lo expande hasta el máximo. Si comienza con un montón de 32 millones y su aplicación alcanzó un máximo de 65 millones, entonces el montón terminará creciendo 32 millones -> 64 millones -> 128 millones.

También puede probar esto para que la VM sea menos agresiva con respecto al crecimiento del almacenamiento dinámico:

-XX:MinHeapFreeRatio=40 Porcentaje mínimo de almacenamiento dinámico 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 el espacio mínimo. Cargando java.net.Socket agregó más de 15M si no recuerdo mal (y probablemente no).


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

La cura es configurar este env. variables:

export MALLOC_ARENA_MAX=4

Si está ejecutando Tomcat, puede agregar esto a TOMCAT_HOME/bin/setenv.sh archivo.

Para Docker, agregue esto a Dockerfile

ENV MALLOC_ARENA_MAX=4

Hay un artículo de IBM sobre la configuración de MALLOC_ARENA_MAXhttps://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

Esta entrada de blog dice

Se sabe que la memoria residente se desliza de manera similar a una fuga de memoria o fragmentación de memoria.

También hay un error de 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 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

Linux
  1. Linux:¿determinar correctamente el uso de memoria en Linux?

  2. Linux:¿Obtener información sobre el uso de memoria de un proceso de /proc/pid/smaps?

  3. Cómo verificar el uso de memoria en un servidor basado en Linux

  4. Uso máximo de memoria de un proceso Linux/Unix

  5. Cómo encontrar el uso de la memoria del usuario en Linux

Cómo verificar el uso de memoria en Linux

Comando libre de Linux (Comprobar el uso de la memoria)

Cómo evitar que PHP-FPM consuma demasiada RAM en Linux

Gestión de memoria de Linux:memoria virtual y paginación de demanda

Determinar correctamente el uso de memoria en Linux

Graficando el uso de la memoria del proceso en Linux