GNU/Linux >> Linux Esercitazione >  >> Linux

Java utilizza molta più memoria rispetto alla dimensione dell'heap (o dimensiona correttamente il limite di memoria Docker)

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 per mezzo di ByteBuffers diretti.

Quindi cosa richiede memoria in un processo Java?

Parti JVM (per lo più mostrate da Native Memory Tracking)

  1. Heap Java

La parte più ovvia. Qui è dove vivono gli oggetti Java. L'heap occupa fino a -Xmx quantità di memoria.

  1. Garbage Collector

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

L'overhead della memoria GC varia tra gli algoritmi GC. -XX:+UseSerialGC e -XX:+UseShenandoahGC hanno il minimo sovraccarico. G1 o CMS possono facilmente utilizzare circa il 10% della dimensione totale dell'heap.

  1. Codice Cache

Contiene codice generato dinamicamente:metodi compilati JIT, interprete e stub di runtime. La sua dimensione è limitata da -XX:ReservedCodeCacheSize (240 milioni per impostazione predefinita). Disattiva -XX:-TieredCompilation per ridurre la quantità di codice compilato e quindi l'utilizzo di Code Cache.

  1. Compilatore

Anche il compilatore JIT stesso richiede memoria per svolgere il proprio lavoro. Questo può essere nuovamente ridotto disattivando la compilazione a livelli o riducendo il numero di thread del compilatore:-XX:CICompilerCount .

  1. Caricamento della classe

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

  1. Tabelle dei simboli

Due hashtable principali della JVM:la tabella dei simboli contiene nomi, firme, identificatori ecc. e la tabella delle stringhe 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 eccessivamente String.intern .

  1. Discussioni

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

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

Buffer diretti

Un'applicazione può richiedere esplicitamente memoria off-heap chiamando ByteBuffer.allocateDirect . Il limite off-heap predefinito è pari a -Xmx , ma può essere sovrascritto con -XX:MaxDirectMemorySize . I ByteBuffer diretti sono inclusi in Other sezione dell'output NMT (o Internal prima di JDK 11).

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

Oltre ai ByteBuffer diretti ci possono essere 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

Codice JNI caricato da System.loadLibrary può allocare tutta la memoria off-heap che desidera 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 ZipInputStream o DirectoryStream .

Agenti JVMTI, in particolare jdwp agente di debug - può anche causare un consumo eccessivo di memoria.

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

Problemi con l'allocatore

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

jemalloc , un allocatore alternativo, spesso appare più intelligente della normale libc malloc , quindi passando a jemalloc può comportare un ingombro inferiore gratuitamente.

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 determinate aree di memoria (come Code Cache) tramite i flag JVM, ma molte altre sono fuori dal controllo di JVM.

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

Aggiorna

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

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


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?

Specificare -Xmx=1g indica alla JVM di allocare un heap da 1 GB. Sta chiedendo alla JVM di limitare l'intero utilizzo della memoria a 1 GB. Ci sono tabelle di carte, cache di codice e ogni sorta di altre strutture di dati off 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 250 MB.

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 anche GC non ha bisogno di rilasciare la memoria utilizzata. Spero XX:MaxRAM ti aiuterà a ridurre l'impronta di memoria. Alla fine, puoi modificare la configurazione del 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 Commited Memory (NMT) per una JVM in esecuzione nel contenitore Docker

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


TL;DR

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

Il footprint di memoria può essere ridotto 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 di classe condiviso può essere disabilitato all'interno di un contenitore poiché le classi non saranno condivise da un altro processo JVM. È possibile utilizzare il seguente flag. Rimuoverà lo spazio di classe condiviso (17 MB).

-Xshare:off

Il raccoglitore di rifiuti serial ha un footprint di memoria minimo al costo di un tempo di pausa più lungo durante l'elaborazione del garbage collect (vedi 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 seguente flag 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 esterna a JVM è ridotta di 80 MB (differenza tra spazio NMT e spazio RSS). Il compilatore di ottimizzazione C2 richiede 100 MB.

I compilatori C1 e C2 può essere disabilitato con il seguente flag.

-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 forte impatto sulle prestazioni dell'applicazione. La disattivazione del compilatore C1 e C2 riduce la memoria utilizzata di 170 MB.

Utilizzo del compilatore Graal VM (sostituzione di C2) porta a un footprint di memoria leggermente inferiore. Aumenta di 20MB lo spazio di memoria del codice e diminuisce di 60MB la memoria JVM esterna.

L'articolo Java Memory Management per 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 nella politica di compilazione avanzata e nella disabilitazione C2 riducono la dimensione della cache del codice di un fattore 5. Alcuni dettagli su Perché una JVM riporta più memoria impegnata rispetto alla dimensione del set residente del processo Linux? quando entrambi i compilatori sono disabilitati.


Linux
  1. Linux:perché Linux mostra sia più che meno memoria di quella che ho installato fisicamente?

  2. Utilizzo della memoria virtuale da Java sotto Linux, troppa memoria utilizzata

  3. Tomcat 7:come impostare correttamente la dimensione dell'heap iniziale?

  4. come controllare la dimensione dell'heap allocata per jvm da linux

  5. Qualche esperienza Java su Raspberry PI?

Come impostare la memoria Docker e il limite di utilizzo della CPU

Usando il comando gratuito di Linux

Come controllare la dimensione dell'heap per un processo su Linux

Limite di memoria e limite della CPU nel contenitore Docker

Come limitare l'utilizzo della memoria dell'applicazione?

Controlla se la dimensione del file è superiore a 1 MB utilizzando la condizione IF