Java utilizza molta più memoria rispetto alla dimensione dell'heap (o dimensione corretta del limite di memoria Docker)


118

Per la mia applicazione, la memoria utilizzata dal processo Java è molto più della dimensione dell'heap.

Il sistema in cui sono in esecuzione i contenitori inizia ad avere problemi di memoria perché il contenitore richiede molta più memoria rispetto alla dimensione dell'heap.

La dimensione dell'heap è impostata su 128 MB ( -Xmx128m -Xms128m) mentre il contenitore occupa fino a 1 GB di memoria. In condizioni normali, sono necessari 500 MB. Se il contenitore docker ha un limite inferiore (ad esempio mem_limit=mem_limit=400MB) il processo viene interrotto dal killer di memoria esaurita del sistema operativo.

Potresti spiegare perché il processo Java utilizza molta più memoria rispetto all'heap? Come dimensionare correttamente il limite di memoria Docker? C'è un modo per ridurre il footprint di memoria fuori heap del processo Java?


Raccolgo alcuni dettagli sul problema utilizzando il comando dal monitoraggio della memoria nativa in JVM .

Dal sistema host, ottengo la memoria utilizzata dal contenitore.

$ 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

Dall'interno del contenitore, ottengo la memoria utilizzata dal processo.

$ 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

L'applicazione è un server web che utilizza Jetty / Jersey / CDI in bundle all'interno di un grasso di ben 36 MB.

Vengono utilizzate le seguenti versioni di OS e Java (all'interno del contenitore). L'immagine Docker è basata su 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


6
L'heap è dove vengono allocati gli oggetti, tuttavia la JVM ha molte altre regioni di memoria comprese le librerie condivise, buffer di memoria diretti, stack di thread, componenti GUI, metaspace. È necessario vedere quanto può essere grande la JVM e rendere il limite abbastanza alto da preferire che il processo muoia piuttosto che usarne altro.
Peter Lawrey

2
Sembra che il GC stia utilizzando molta memoria. Potresti invece provare a utilizzare il raccoglitore CMS. Sembra che ~ 125 MB siano usati per metaspace + codice, tuttavia senza ridurre la tua base di codice, è improbabile che tu sia in grado di ridurlo. Lo spazio impegnato è vicino al tuo limite, quindi non sorprende che venga ucciso.
Peter Lawrey

dove / come si impostano le configurazioni -Xms e -Xmx?
Mick


1
Il tuo programma esegue molte operazioni sui file (es. Crea file di dimensioni gigabyte)? Se è così, dovresti sapere che cgroupsaggiunge la cache del disco alla memoria utilizzata, anche se è gestita dal kernel ed è invisibile per il programma utente. (Intendiamoci, comandi pse docker statsnon contare la cache del disco.)
Lorinczy Zsigmond il

Risposte:


207

La memoria virtuale utilizzata da un processo Java si estende ben oltre il semplice Java Heap. Sai, JVM include molti sottosistemi: Garbage Collector, Class Loading, compilatori JIT ecc. E tutti questi sottosistemi richiedono una certa quantità di RAM per funzionare.

JVM non è l'unico consumatore di RAM. Anche le librerie native (inclusa la libreria di classi Java standard) possono allocare memoria nativa. E questo non sarà nemmeno visibile al Native Memory Tracking. L'applicazione Java stessa può anche utilizzare la memoria off-heap tramite ByteBuffer diretti.

Quindi cosa richiede memoria in un processo Java?

Parti JVM (mostrate principalmente da Native Memory Tracking)

  1. Java Heap

    La parte più ovvia. Qui è dove vivono gli oggetti Java. Heap richiede fino a -Xmxquantità di memoria.

  2. Netturbino

    Le strutture e gli algoritmi GC richiedono memoria aggiuntiva per la gestione dell'heap. Queste strutture sono Mark Bitmap, Mark Stack (per attraversare l'oggetto grafico), Remembered Sets (per registrare i riferimenti interregionali) e altri. Alcuni di essi sono sintonizzabili direttamente, ad esempio -XX:MarkStackSizeMax, altri dipendono dal layout dell'heap, ad esempio più grandi sono le regioni G1 ( -XX:G1HeapRegionSize), più piccoli sono gli insiemi ricordati.

    Il sovraccarico della memoria GC varia tra gli algoritmi GC. -XX:+UseSerialGCe -XX:+UseShenandoahGChanno il più piccolo overhead. G1 o CMS possono facilmente utilizzare circa il 10% della dimensione totale dell'heap.

  3. Cache del codice

    Contiene codice generato dinamicamente: metodi compilati JIT, interprete e stub di runtime. Le sue dimensioni sono limitate da -XX:ReservedCodeCacheSize(240 M per impostazione predefinita). Disattivare -XX:-TieredCompilationper ridurre la quantità di codice compilato e quindi l'utilizzo della cache del codice.

  4. Compiler

    Anche il compilatore JIT stesso richiede memoria per svolgere il suo lavoro. Questo può essere ridotto nuovamente spegnendo a file compilazione o riducendo il numero di fili compilatore: -XX:CICompilerCount.

  5. Caricamento della classe

    I metadati della classe (bytecode del metodo, simboli, pool di costanti, annotazioni, ecc.) Sono archiviati in un'area fuori heap chiamata Metaspace. Più classi vengono caricate, più metaspace viene utilizzato. L'utilizzo totale può essere limitato da -XX:MaxMetaspaceSize(illimitato per impostazione predefinita) e -XX:CompressedClassSpaceSize(1G per impostazione predefinita).

  6. Tabelle dei simboli

    Due tabelle hash principali della JVM: la tabella Symbol contiene nomi, firme, identificatori ecc. E la tabella String contiene riferimenti a stringhe interne. Se Native Memory Tracking indica un utilizzo significativo della memoria da parte di una tabella String, probabilmente significa che l'applicazione chiama in modo eccessivo String.intern.

  7. discussioni

    Gli stack di thread sono anche responsabili dell'assunzione della RAM. La dimensione dello stack è controllata da -Xss. L'impostazione predefinita è 1 M per thread, ma fortunatamente le cose non vanno così male. Il sistema operativo alloca pigramente le pagine di memoria, cioè al primo utilizzo, quindi l'utilizzo effettivo della memoria sarà molto inferiore (tipicamente 80-200 KB per stack di thread). Ho scritto uno script per stimare quanto RSS appartiene agli stack di thread Java.

    Ci sono altre parti JVM che allocano la memoria nativa, ma di solito non giocano un ruolo importante nel consumo totale di memoria.

Buffer diretti

Un'applicazione può richiedere esplicitamente memoria fuori heap chiamando ByteBuffer.allocateDirect. Il limite di off-heap predefinito è uguale a -Xmx, ma può essere sovrascritto con -XX:MaxDirectMemorySize. I ByteBuffer diretti sono inclusi nella Othersezione dell'output NMT (o Internalprima di JDK 11).

La quantità di memoria diretta utilizzata è visibile tramite JMX, ad esempio in JConsole o Java Mission Control:

BufferPool MBean

Oltre ai ByteBuffer diretti, possono esserci MappedByteBuffers- i file mappati nella memoria virtuale di un processo. NMT non li tiene traccia, tuttavia, MappedByteBuffers può anche occupare memoria fisica. E non esiste un modo semplice per limitare quanto possono prendere. Puoi solo vedere l'utilizzo effettivo guardando la mappa della memoria del processo: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
                           ^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^

Librerie native

Il codice JNI caricato da System.loadLibrarypuò allocare tutta la memoria fuori heap che vuole senza alcun controllo dal lato JVM. Ciò riguarda anche la libreria di classi Java standard. In particolare, le risorse Java non chiuse possono diventare una fonte di perdita di memoria nativa. Esempi tipici sono ZipInputStreamo DirectoryStream.

Gli agenti JVMTI, in particolare l' jdwpagente di debug, possono anche causare un consumo eccessivo di memoria.

Questa risposta descrive come profilare le allocazioni di memoria nativa con async-profiler .

Problemi di allocatore

Un processo tipicamente richiede la memoria nativa direttamente dal sistema operativo (tramite mmapchiamata di sistema) o utilizzando malloc- l'allocatore libc standard. A sua volta, mallocrichiede grossi blocchi di memoria dal sistema operativo utilizzando mmape quindi li gestisce in base al proprio algoritmo di allocazione. Il problema è che questo algoritmo può portare alla frammentazione e all'utilizzo eccessivo della memoria virtuale .

jemalloc, un allocatore alternativo, spesso appare più intelligente della normale libc malloc, quindi il passaggio a jemallocpuò comportare gratuitamente un footprint inferiore.

Conclusione

Non esiste un modo garantito per stimare l'utilizzo completo della memoria di un processo Java, perché ci sono troppi fattori da considerare.

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

È possibile ridurre o limitare alcune aree di memoria (come Code Cache) tramite flag JVM, ma molte altre sono fuori dal controllo della JVM.

Un possibile approccio per impostare i limiti di Docker sarebbe osservare l'utilizzo effettivo della memoria in uno stato "normale" del processo. Esistono strumenti e tecniche per analizzare i problemi con il consumo di memoria Java: Native Memory Tracking , pmap , jemalloc , async-profiler .

Aggiornare

Ecco una registrazione della mia presentazione Memory Footprint of a Java Process .

In questo video discuto di cosa può consumare memoria in un processo Java, come monitorare e limitare la dimensione di determinate aree di memoria e come profilare le perdite di memoria nativa in un'applicazione Java.


1
Non sono stringhe internate nell'heap a partire da jdk7? ( bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931 ) - forse mi sbaglio.
j-keck

5
@ j-keck Gli oggetti stringa sono nell'heap, ma la tabella hash (i bucket e le voci con riferimenti e codici hash) si trova nella memoria off-heap. Ho riformulato la frase per essere più preciso. Grazie per avermelo fatto notare.
apangin

per aggiungere a questo, anche se si utilizzano ByteBuffer non diretti, la JVM allocherà buffer diretti temporanei nella memoria nativa senza limiti di memoria imposti. Cf. evanjones.ca/java-bytebuffer-leak.html
Cpt. Senkfuss

16

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

Perché quando specifico -Xmx = 1g la mia JVM utilizza più memoria di 1 GB di memoria?

Specificando -Xmx = 1g si dice alla JVM di allocare un heap da 1 GB. Non sta dicendo alla JVM di limitare il suo intero utilizzo della memoria a 1 GB. Ci sono tabelle di carte, cache di codice e tutti i tipi di altre strutture di dati fuori heap. Il parametro utilizzato per specificare l'utilizzo totale della memoria è -XX: MaxRAM. Tieni presente che con -XX: MaxRam = 500m il tuo heap sarà di circa 250mb.

Java vede la dimensione della memoria dell'host e non è a conoscenza di alcuna limitazione della memoria del contenitore. Non crea pressione sulla memoria, quindi GC non ha bisogno di rilasciare la memoria utilizzata. Spero XX:MaxRAMti possa aiutare a ridurre l'impronta di memoria. Alla fine, è possibile modificare la configurazione GC ( -XX:MinHeapFreeRatio, -XX:MaxHeapFreeRatio, ...)


Esistono molti tipi di metriche di memoria. Docker sembra segnalare la dimensione della memoria RSS, che può essere diversa dalla memoria "impegnata" riportata da jcmd(le versioni precedenti di Docker riportano RSS + cache come utilizzo della memoria). Buona discussione e collegamenti: Differenza tra Resident Set Size (RSS) e Java total committed memory (NMT) per una JVM in esecuzione nel contenitore Docker

La memoria (RSS) può essere consumata anche da altre utilità nel contenitore: shell, gestore dei processi, ... Non sappiamo cos'altro è in esecuzione nel contenitore e come si avvia i processi nel contenitore.


È davvero meglio con -XX:MaxRam. Penso che stia ancora usando più del massimo definito ma è meglio, grazie!
Nicolas Henneaux

Forse hai davvero bisogno di più memoria per questa istanza Java. Ci sono 15267 classi, 56 thread.
Jan Garaj

1
Qui ci sono maggiori dettagli, argomenti Java -Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC, produce Docker 428.5MiB / 600MiBe jcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB. JVM richiede circa 300 MB mentre il container richiede 430 MB. Dove sono i 130 MB tra il reporting JVM e il reporting del sistema operativo?
Nicolas Henneaux

1
Aggiunte informazioni / link sulla memoria RSS.
Jan Garaj

L'RSS fornito proviene dall'interno del contenitore per il processo Java solo ps -p 71 -o pcpu,rss,size,vsizecon il processo Java con pid 71. In realtà -XX:MaxRamnon è stato d'aiuto ma il collegamento che hai fornito aiuta con GC seriale.
Nicolas Henneaux

8

TL; DR

L'utilizzo dettagliato della memoria è fornito dai dettagli NMT (Native Memory Tracking) (principalmente metadati del codice e garbage collector). Inoltre, il compilatore e l'ottimizzatore Java C1 / C2 consumano la memoria non riportata nel riepilogo.

L'impronta di memoria può essere ridotta utilizzando i flag JVM (ma ci sono impatti).

Il dimensionamento del contenitore Docker deve essere eseguito tramite test con il carico previsto dell'applicazione.


Dettaglio per ogni componente

Lo spazio della classe condiviso può essere disabilitato all'interno di un contenitore poiché le classi non saranno condivise da un altro processo JVM. È possibile utilizzare il flag seguente. Rimuoverà lo spazio della classe condiviso (17 MB).

-Xshare:off

Il numero di serie del garbage collector ha un ingombro di memoria minimo al costo di un tempo di pausa più lungo durante l'elaborazione della raccolta dei rifiuti (vedere il confronto di Aleksey Shipilëv tra GC in un'immagine ). Può essere abilitato con il seguente flag. Può risparmiare fino allo spazio GC utilizzato (48 MB).

-XX:+UseSerialGC

Il compilatore C2 può essere disabilitato con il flag seguente per ridurre i dati di profilazione utilizzati per decidere se ottimizzare o meno un metodo.

-XX:+TieredCompilation -XX:TieredStopAtLevel=1

Lo spazio del codice è ridotto di 20 MB. Inoltre, la memoria al di fuori della JVM è ridotta di 80 MB (differenza tra spazio NMT e spazio RSS). Il compilatore di ottimizzazione C2 necessita di 100 MB.

I compilatori C1 e C2 possono essere disabilitati con il flag seguente.

-Xint

La memoria all'esterno della JVM è ora inferiore allo spazio totale impegnato. Lo spazio del codice è ridotto di 43 MB. Attenzione, questo ha un impatto importante sulle prestazioni dell'applicazione. La disabilitazione del compilatore C1 e C2 riduce la memoria utilizzata di 170 MB.

L'uso del compilatore Graal VM (sostituzione di C2) porta a un ingombro di memoria leggermente inferiore. Aumenta di 20 MB lo spazio di memoria del codice e diminuisce di 60 MB dall'esterno della memoria JVM.

L'articolo Java Memory Management for JVM fornisce alcune informazioni rilevanti sui diversi spazi di memoria. Oracle fornisce alcuni dettagli nella documentazione di Native Memory Tracking . Maggiori dettagli sul livello di compilazione nei criteri di compilazione avanzati e nella disabilitazione di C2 riducono la dimensione della cache del codice di un fattore 5 . Alcuni dettagli su Perché una JVM segnala più memoria impegnata rispetto alla dimensione del set residente del processo Linux? quando entrambi i compilatori sono disabilitati.


-1

Java ha bisogno di molta memoria. La stessa JVM necessita di molta memoria per essere eseguita. L'heap è la memoria disponibile all'interno della macchina virtuale, disponibile per la tua applicazione. Poiché JVM è un grande pacchetto pieno di tutte le chicche possibili, richiede molta memoria solo per il caricamento.

A partire da java 9 hai qualcosa chiamato project Jigsaw , che potrebbe ridurre la memoria utilizzata quando avvii un'app java (insieme all'ora di inizio). Il puzzle del progetto e un nuovo sistema di moduli non sono stati necessariamente creati per ridurre la memoria necessaria, ma se è importante puoi provare.

Puoi dare un'occhiata a questo esempio: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . Utilizzando il sistema del modulo è risultata in un'applicazione CLI di 21 MB (con JRE incorporato). JRE impiega più di 200 MB. Ciò dovrebbe tradursi in una quantità di memoria allocata inferiore quando l'applicazione è attiva (molte classi JRE inutilizzate non verranno più caricate).

Ecco un altro bel tutorial: https://www.baeldung.com/project-jigsaw-java-modularity

Se non vuoi perdere tempo con questo, puoi semplicemente allocare più memoria. A volte è il migliore.


L'utilizzo jlinkè piuttosto restrittivo in quanto richiedeva la modularizzazione dell'applicazione. Il modulo automatico non è supportato quindi non esiste un modo semplice per arrivarci.
Nicolas Henneaux

-1

Come dimensionare correttamente il limite di memoria Docker? Controlla l'applicazione monitorandola per un po 'di tempo. Per limitare la memoria del contenitore, prova a utilizzare -m, --memory bytes opzione per il comando docker run - o qualcosa di equivalente se lo stai eseguendo altrimenti come

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

non può rispondere ad altre domande.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.