memoria maquina aumentar java linux docker memory jvm

maquina - Java usa mucha más memoria que el tamaño del almacenamiento dinámico(o el tamaño correcto del límite de memoria de Docker)



aumentar memoria java (6)

Para mi aplicación, la memoria utilizada por el proceso de Java es mucho más que el tamaño del montón.

El sistema donde se ejecutan los contenedores comienza a tener problemas de memoria porque el contenedor está tomando mucha más memoria que el tamaño del montón.

El tamaño del montón se establece en 128 MB ( -Xmx128m -Xms128m ), mientras que el contenedor ocupa hasta 1 GB de memoria. En condiciones normales, necesita 500MB. Si el contenedor de la mem_limit=mem_limit=400MB acoplable tiene un límite inferior (por ejemplo, mem_limit=mem_limit=400MB ), el asesino que se queda sin memoria del sistema operativo mem_limit=mem_limit=400MB el proceso.

¿Podría explicar por qué el proceso de Java utiliza mucha más memoria que el montón? ¿Cómo dimensionar correctamente el límite de memoria Docker? ¿Hay una manera de reducir la huella de memoria fuera del montón del proceso de Java?

Recopilé algunos detalles sobre el problema usando el comando de seguimiento de memoria nativa en JVM .

Desde el sistema host, obtengo la memoria utilizada por el contenedor.

$ docker stats --no-stream 9afcb62a26c8 CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 9afcb62a26c8 xx-xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.0acbb46bb6fe3ae1b1c99aff3a6073bb7b7ecf85 0.93% 461MiB / 9.744GiB 4.62% 286MB / 7.92MB 157MB / 2.66GB 57

Desde el interior del contenedor, obtengo la memoria utilizada por el proceso.

$ ps -p 71 -o pcpu,rss,size,vsize %CPU RSS SIZE VSZ 11.2 486040 580860 3814600

$ jcmd 71 VM.native_memory 71: Native Memory Tracking: Total: reserved=1631932KB, committed=367400KB - Java Heap (reserved=131072KB, committed=131072KB) (mmap: reserved=131072KB, committed=131072KB) - Class (reserved=1120142KB, committed=79830KB) (classes #15267) ( instance classes #14230, array classes #1037) (malloc=1934KB #32977) (mmap: reserved=1118208KB, committed=77896KB) ( Metadata: ) ( reserved=69632KB, committed=68272KB) ( used=66725KB) ( free=1547KB) ( waste=0KB =0.00%) ( Class space:) ( reserved=1048576KB, committed=9624KB) ( used=8939KB) ( free=685KB) ( waste=0KB =0.00%) - Thread (reserved=24786KB, committed=5294KB) (thread #56) (stack: reserved=24500KB, committed=5008KB) (malloc=198KB #293) (arena=88KB #110) - Code (reserved=250635KB, committed=45907KB) (malloc=2947KB #13459) (mmap: reserved=247688KB, committed=42960KB) - GC (reserved=48091KB, committed=48091KB) (malloc=10439KB #18634) (mmap: reserved=37652KB, committed=37652KB) - Compiler (reserved=358KB, committed=358KB) (malloc=249KB #1450) (arena=109KB #5) - Internal (reserved=1165KB, committed=1165KB) (malloc=1125KB #3363) (mmap: reserved=40KB, committed=40KB) - Other (reserved=16696KB, committed=16696KB) (malloc=16696KB #35) - Symbol (reserved=15277KB, committed=15277KB) (malloc=13543KB #180850) (arena=1734KB #1) - Native Memory Tracking (reserved=4436KB, committed=4436KB) (malloc=378KB #5359) (tracking overhead=4058KB) - Shared class space (reserved=17144KB, committed=17144KB) (mmap: reserved=17144KB, committed=17144KB) - Arena Chunk (reserved=1850KB, committed=1850KB) (malloc=1850KB) - Logging (reserved=4KB, committed=4KB) (malloc=4KB #179) - Arguments (reserved=19KB, committed=19KB) (malloc=19KB #512) - Module (reserved=258KB, committed=258KB) (malloc=258KB #2356) $ cat /proc/71/smaps | grep Rss | cut -d: -f2 | tr -d " " | cut -f1 -dk | sort -n | awk ''{ sum += $1 } END { print sum }'' 491080

La aplicación es un servidor web que utiliza Jetty / Jersey / CDI incluido dentro de una grasa de 36 MB.

Se utiliza la siguiente versión de SO y Java (dentro del contenedor). La imagen de Docker se basa en openjdk:11-jre-slim .

$ java -version openjdk version "11" 2018-09-25 OpenJDK Runtime Environment (build 11+28-Debian-1) OpenJDK 64-Bit Server VM (build 11+28-Debian-1, mixed mode, sharing) $ uname -a Linux service1 4.9.125-linuxkit #1 SMP Fri Sep 7 08:20:28 UTC 2018 x86_64 GNU/Linux

https://gist.github.com/prasanthj/48e7063cac88eb396bc9961fb3149b58


TL; DR

El uso detallado de la memoria es proporcionado por detalles de seguimiento de memoria nativa (NMT) (principalmente metadatos de código y recolector de basura). Además de eso, el compilador y el optimizador de Java C1 / C2 consumen la memoria que no se informa en el resumen.

La huella de la memoria se puede reducir utilizando indicadores JVM (pero hay impactos).

El tamaño del contenedor Docker debe realizarse a través de pruebas con la carga esperada de la aplicación.

Detalle para cada componentes.

El espacio de clase compartido se puede deshabilitar dentro de un contenedor, ya que las clases no serán compartidas por otro proceso de JVM. Se puede utilizar la siguiente bandera. Se eliminará el espacio de clase compartido (17MB).

-Xshare:off

El colector de basura en serie tiene una huella de memoria mínima al costo de un tiempo de pausa más largo durante el procesamiento de recolección de basura (consulte la comparación de Aleksey Shipilëv entre GC en una imagen ). Se puede habilitar con la siguiente bandera. Puede ahorrar hasta el espacio GC utilizado (48MB).

-XX:+UseSerialGC

El compilador C2 se puede deshabilitar con el siguiente indicador para reducir los datos de perfil utilizados para decidir si optimizar o no un método.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

El espacio de código se reduce en 20 MB. Además, la memoria fuera de JVM se reduce en 80 MB (diferencia entre el espacio NMT y el espacio RSS). El compilador optimizador C2 necesita 100MB.

Los compiladores C1 y C2 se pueden deshabilitar con el siguiente indicador.

-Xint

La memoria fuera de la JVM ahora es más baja que el espacio total comprometido. El espacio de código se reduce en 43 MB. Cuidado, esto tiene un gran impacto en el rendimiento de la aplicación. Desactivar el compilador C1 y C2 reduce la memoria utilizada por 170 MB.

El uso del compilador Graal VM (reemplazo de C2) lleva a una huella de memoria un poco más pequeña. Aumenta de 20 MB el espacio de memoria de código y disminuye de 60 MB desde la memoria exterior de la JVM.

El artículo Java Memory Management for JVM proporciona información relevante sobre los diferentes espacios de memoria. Oracle proporciona algunos detalles en la documentación de seguimiento de memoria nativa . Más detalles sobre el nivel de compilación en la política de compilación avanzada y en deshabilitar C2 reducen el tamaño de la memoria caché del código en un factor 5 . Algunos detalles sobre ¿Por qué una JVM reporta más memoria comprometida que el tamaño del conjunto residente del proceso de Linux? cuando ambos compiladores están deshabilitados.


Java necesita mucha memoria. JVM necesita mucha memoria para ejecutarse. El montón es la memoria que está disponible dentro de la máquina virtual, disponible para su aplicación. Debido a que JVM es un paquete grande con todas las ventajas posibles, se necesita mucha memoria solo para cargar.

Comenzando con java 9, tiene algo llamado proyecto Jigsaw , que podría reducir la memoria utilizada al iniciar una aplicación java (junto con la hora de inicio). El rompecabezas del proyecto y un nuevo sistema de módulos no se crearon necesariamente para reducir la memoria necesaria, pero si es importante, puede intentarlo.

Puede ver este ejemplo: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . Al utilizar el sistema de módulos, se obtuvo una aplicación CLI de 21 MB (con JRE incrustado). JRE lleva más de 200mb. Eso debería traducirse en menos memoria asignada cuando la aplicación está activa (ya no se cargarán muchas clases de JRE no utilizadas).

Aquí hay otro buen tutorial: https://www.baeldung.com/project-jigsaw-java-modularity

Si no desea dedicar tiempo a esto, simplemente puede asignar más memoria. A veces es lo mejor.


La memoria virtual utilizada por un proceso Java se extiende mucho más allá de Java Heap. Ya sabes, JVM incluye muchos subsistemas: recolector de basura, carga de clases, compiladores JIT, etc., y todos estos subsistemas requieren cierta cantidad de RAM para funcionar.

JVM no es el único consumidor de RAM. Las bibliotecas nativas (incluida la biblioteca estándar de clases de Java) también pueden asignar memoria nativa. Y esto ni siquiera será visible para el seguimiento de memoria nativa. La aplicación Java en sí misma también puede usar memoria fuera de pila mediante ByteBuffers directos.

Entonces, ¿qué lleva la memoria en un proceso de Java?

Partes de JVM (en su mayoría mostradas por Native Memory Tracking)

  1. Montón de Java

    La parte más obvia. Aquí es donde viven los objetos de Java. Heap toma hasta -Xmx cantidad de memoria.

  2. Recolector de basura

    Las estructuras y los algoritmos de GC requieren memoria adicional para la administración del montón. Estas estructuras son Mark Bitmap, Mark Stack (para trazar el gráfico de objetos), Conjuntos recordados (para registrar referencias entre regiones) y otros. Algunos de ellos se pueden -XX:MarkStackSizeMax directamente, por ejemplo, -XX:MarkStackSizeMax , otros dependen del diseño del montón, por ejemplo, cuanto más grandes son las regiones G1 ( -XX:G1HeapRegionSize ), más pequeños son los conjuntos recordados.

    La sobrecarga de la memoria del GC varía entre los algoritmos del GC. -XX:+UseSerialGC y -XX:+UseShenandoahGC tiene la sobrecarga más pequeña. G1 o CMS pueden usar fácilmente alrededor del 10% del tamaño total del montón.

  3. Código de caché

    Contiene código generado dinámicamente: métodos compilados por JIT, intérprete y apéndices en tiempo de ejecución. Su tamaño está limitado por -XX:ReservedCodeCacheSize (240M por defecto). Desactive -XX:-TieredCompilation para reducir la cantidad de código compilado y, por lo tanto, el uso de la caché de código.

  4. Compilador

    El compilador JIT en sí también requiere memoria para hacer su trabajo. Esto se puede reducir de nuevo desactivando la Compilación por niveles o reduciendo el número de subprocesos del compilador: -XX:CICompilerCount .

  5. Carga de clases

    Los metadatos de clase (métodos de bytecodes, símbolos, grupos de constantes, anotaciones, etc.) se almacenan en un área fuera de almacenamiento denominada Metaspace. Cuantas más clases se carguen, más metaspacio se usará. El uso total puede estar limitado por -XX:MaxMetaspaceSize (ilimitado por defecto) y -XX:CompressedClassSpaceSize (1G por defecto).

  6. Tablas de simbolos

    Dos tablas hash principales de la JVM: la tabla de símbolos contiene nombres, firmas, identificadores, etc. y la tabla de cadenas contiene referencias a cadenas internadas. Si el seguimiento de la memoria nativa indica un uso significativo de la memoria por parte de una tabla de cadenas, probablemente significa que la aplicación llama excesivamente a String.intern .

  7. Trapos

    Pilas de hilo también son responsables de tomar RAM. El tamaño de pila es controlado por -Xss . El valor predeterminado es 1M por hilo, pero afortunadamente las cosas no están tan mal. El sistema operativo asigna las páginas de la memoria de forma perezosa, es decir, en el primer uso, por lo que el uso real de la memoria será mucho menor (normalmente 80-200 KB por pila de subprocesos). Escribí un script para estimar cuánto de RSS pertenece a las pilas de hilos de Java.

    Hay otras partes de JVM que asignan memoria nativa, pero generalmente no juegan un papel importante en el consumo total de memoria.

Buffers directos

Una aplicación puede solicitar explícitamente la memoria fuera del montón llamando a ByteBuffer.allocateDirect . El límite predeterminado de -Xmx fuera del montón es igual a -Xmx , pero se puede anular con -XX:MaxDirectMemorySize . Los ByteBuffers directos se incluyen en la sección Other de la salida de NMT (o Internal antes de JDK 11).

La cantidad de memoria directa utilizada es visible a través de JMX, por ejemplo, en JConsole o Java Mission Control:

Además de los ByteBuffers directos, puede haber MappedByteBuffers , los archivos asignados a la memoria virtual de un proceso. NMT no los rastrea, sin embargo, MappedByteBuffers también puede tomar memoria física. Y no hay una forma sencilla de limitar la cantidad que pueden tomar. Puede ver el uso real mirando el mapa de memoria de proceso: pmap -x <pid>

Address Kbytes RSS Dirty Mode Mapping ... 00007f2b3e557000 39592 32956 0 r--s- some-file-17405-Index.db 00007f2b40c01000 39600 33092 0 r--s- some-file-17404-Index.db ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^

Bibliotecas nativas

El código JNI cargado por System.loadLibrary puede asignar tanta memoria fuera de almacenamiento como desee sin control desde el lado de JVM. Esto también concierne a la biblioteca estándar de clases de Java. En particular, los recursos Java no cerrados pueden convertirse en una fuente de pérdida de memoria nativa. Ejemplos típicos son ZipInputStream o DirectoryStream .

Los agentes JVMTI, en particular, el agente de depuración jdwp , también pueden causar un consumo excesivo de memoria.

Esta respuesta describe cómo perfilar las asignaciones de memoria nativas con async-profiler .

Problemas de asignación

Normalmente, un proceso solicita memoria nativa ya sea directamente desde el sistema operativo (mediante una llamada al sistema mmap ) o mediante el uso de malloc : asignador libc estándar. A su vez, malloc solicita grandes trozos de memoria al sistema operativo usando mmap , y luego administra estos trozos de acuerdo con su propio algoritmo de asignación. El problema es que este algoritmo puede conducir a la fragmentación y al uso excesivo de la memoria virtual .

jemalloc , un asignador alternativo, a menudo parece más inteligente que el libc malloc regular, por lo que cambiar a jemalloc puede resultar en una huella más pequeña de forma gratuita.

Conclusión

No hay una forma garantizada de estimar el uso de la memoria completa de un proceso Java, porque hay demasiados factores a considerar.

Total memory = Heap + Code Cache + Metaspace + Symbol tables + Other JVM structures + Thread stacks + Direct buffers + Mapped files + Native Libraries + Malloc overhead + ...

Es posible reducir o limitar ciertas áreas de memoria (como la caché de código) mediante indicadores JVM, pero muchos otros están fuera del control de JVM.

Un posible enfoque para establecer los límites de Docker sería observar el uso real de la memoria en un estado "normal" del proceso. Existen herramientas y técnicas para investigar problemas con el consumo de memoria de Java: Seguimiento de memoria nativa , pmap , jemalloc , async-profiler .



https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :

¿Por qué cuando especifico -Xmx = 1g mi JVM usa más memoria que 1gb de memoria?

Especificar -Xmx = 1g le dice a la JVM que asigne un montón de 1 gb. No le está diciendo a la JVM que limite su uso de memoria completa a 1 gb. Hay tablas de tarjetas, cachés de código y todo tipo de otras estructuras de datos fuera del montón. El parámetro que utiliza para especificar el uso total de memoria es -XX: MaxRAM. Tenga en cuenta que con -XX: MaxRam = 500 m su montón será de aproximadamente 250 mb.

Java ve el tamaño de la memoria del host y no tiene conocimiento de ninguna limitación de la memoria del contenedor. No crea presión en la memoria, por lo que GC tampoco necesita liberar la memoria usada. Espero que XX:MaxRAM te ayude a reducir la huella de memoria. Eventualmente, puede modificar la configuración del GC ( -XX:MinHeapFreeRatio , -XX:MaxHeapFreeRatio , ...)

Hay muchos tipos de métricas de memoria. Docker parece estar reportando el tamaño de la memoria RSS, que puede ser diferente a la memoria "confirmada" reportada por jcmd (las versiones anteriores de Docker reportan el almacenamiento en caché RSS + como uso de memoria). Buena discusión y enlaces: Diferencia entre el tamaño del conjunto residente (RSS) y la memoria comprometida total de Java (NMT) para una JVM que se ejecuta en el contenedor Docker

La memoria (RSS) también puede ser consumida por otras utilidades en el contenedor: shell, administrador de procesos, ... No sabemos qué más se está ejecutando en el contenedor y cómo iniciar los procesos en el contenedor.


¿Cómo dimensionar correctamente el límite de memoria Docker? Verifique la aplicación monitorizándola por algún tiempo. Para restringir la memoria del contenedor, intente usar -m, la opción de memoria en bytes para el comando de ejecución de la ventana acoplable, o algo equivalente si lo está ejecutando de otra manera

docker run -d --name my-container --memory 500m <iamge-name>

No puedo responder otras preguntas.