Pour mon application, la mémoire utilisée par le processus Java est bien supérieure à la taille du tas.
Le système sur lequel les conteneurs sont exécutés commence à avoir des problèmes de mémoire car le conteneur utilise beaucoup plus de mémoire que la taille du tas.
La taille du tas est définie sur 128 Mo ( -Xmx128m -Xms128m
) tandis que le conteneur occupe jusqu'à 1 Go de mémoire. Dans des conditions normales, il a besoin de 500 Mo. Si le conteneur docker a une limite inférieure (par exemple mem_limit=mem_limit=400MB
), le processus est tué par le tueur de mémoire insuffisante du système d'exploitation.
Pouvez-vous expliquer pourquoi le processus Java utilise beaucoup plus de mémoire que le tas? Comment dimensionner correctement la limite de mémoire Docker? Existe-t-il un moyen de réduire l'empreinte mémoire hors tas du processus Java?
Je rassemble quelques détails sur le problème à l'aide de la commande du suivi de la mémoire native dans JVM .
Du système hôte, j'obtiens la mémoire utilisée par le conteneur.
$ 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
De l'intérieur du conteneur, j'obtiens la mémoire utilisée par le processus.
$ 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'application est un serveur Web utilisant Jetty / Jersey / CDI dans un gros lointain de 36 Mo.
La version suivante du système d'exploitation et de Java est utilisée (à l'intérieur du conteneur). L'image Docker est basée sur 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
cgroups
ajoute un cache disque à la mémoire utilisée - même s'il est géré par le noyau et qu'il est invisible pour le programme utilisateur. (Attention, commandesps
etdocker stats
ne comptez pas le cache disque.)Réponses:
La mémoire virtuelle utilisée par un processus Java va bien au-delà du simple Java Heap. Vous savez, JVM comprend de nombreux sous-systèmes: Garbage Collector, Class Loading, JIT compilers etc., et tous ces sous-systèmes nécessitent une certaine quantité de RAM pour fonctionner.
JVM n'est pas le seul consommateur de RAM. Les bibliothèques natives (y compris la bibliothèque de classes Java standard) peuvent également allouer de la mémoire native. Et cela ne sera même pas visible pour le suivi de la mémoire native. L'application Java elle-même peut également utiliser de la mémoire hors tas au moyen de ByteBuffers directs.
Alors, qu'est-ce qui prend de la mémoire dans un processus Java?
Pièces JVM (principalement affichées par Native Memory Tracking)
Tas Java
La partie la plus évidente. C'est là que vivent les objets Java. Heap prend jusqu'à la
-Xmx
quantité de mémoire.Éboueur
Les structures et algorithmes GC nécessitent une mémoire supplémentaire pour la gestion du tas. Ces structures sont Mark Bitmap, Mark Stack (pour parcourir le graphe d'objets), Remembered Sets (pour enregistrer des références inter-régions) et autres. Certains d'entre eux sont directement réglables, par exemple
-XX:MarkStackSizeMax
, d'autres dépendent de la disposition du tas, par exemple plus les régions G1 sont grandes (-XX:G1HeapRegionSize
), plus les ensembles sont mémorisés plus petits.La surcharge de mémoire GC varie selon les algorithmes GC.
-XX:+UseSerialGC
et-XX:+UseShenandoahGC
ont les plus petits frais généraux. G1 ou CMS peuvent facilement utiliser environ 10% de la taille totale du tas.Cache de code
Contient du code généré dynamiquement: méthodes compilées par JIT, interpréteur et stubs d'exécution. Sa taille est limitée par
-XX:ReservedCodeCacheSize
(240M par défaut). Désactivez-XX:-TieredCompilation
pour réduire la quantité de code compilé et donc l'utilisation du cache de code.Compilateur
Le compilateur JIT lui-même a également besoin de mémoire pour faire son travail. Ceci peut être réduit à nouveau en coupant Tiered Compilation ou en réduisant le nombre de fils de compilateur:
-XX:CICompilerCount
.Chargement de classe
Les métadonnées de classe (bytecodes de méthode, symboles, pools de constantes, annotations, etc.) sont stockées dans une zone hors tas appelée Metaspace. Plus il y a de classes chargées - plus la métaspace est utilisée. L'utilisation totale peut être limitée par
-XX:MaxMetaspaceSize
(illimité par défaut) et-XX:CompressedClassSpaceSize
(1G par défaut).Tables de symboles
Deux principales tables de hachage de la JVM: la table Symbol contient des noms, des signatures, des identifiants, etc. et la table String contient des références à des chaînes internées. Si le suivi de la mémoire native indique une utilisation importante de la mémoire par une table String, cela signifie probablement que l'application appelle excessivement
String.intern
.Fils
Les piles de threads sont également responsables de la prise de RAM. La taille de la pile est contrôlée par
-Xss
. La valeur par défaut est de 1M par thread, mais heureusement, les choses ne sont pas si mauvaises. Le système d'exploitation alloue les pages de mémoire paresseusement, c'est-à-dire lors de la première utilisation, de sorte que l'utilisation réelle de la mémoire sera beaucoup plus faible (généralement 80-200 Ko par pile de threads). J'ai écrit un script pour estimer la part de RSS appartenant aux piles de threads Java.Il existe d'autres composants JVM qui allouent de la mémoire native, mais ils ne jouent généralement pas un rôle important dans la consommation totale de mémoire.
Tampons directs
Une application peut demander explicitement de la mémoire hors tas en appelant
ByteBuffer.allocateDirect
. La limite par défaut hors tas est égale à-Xmx
, mais elle peut être remplacée par-XX:MaxDirectMemorySize
. Les ByteBuffers directs sont inclus dans laOther
section de sortie NMT (ouInternal
avant JDK 11).La quantité de mémoire directe utilisée est visible via JMX, par exemple dans JConsole ou Java Mission Control:
En plus des ByteBuffers directs, il peut y avoir
MappedByteBuffers
- les fichiers mappés à la mémoire virtuelle d'un processus. NMT ne les suit pas, cependant, MappedByteBuffers peut également prendre de la mémoire physique. Et il n'y a pas de moyen simple de limiter ce qu'ils peuvent prendre. Vous pouvez simplement voir l'utilisation réelle en regardant la carte mémoire du processus:pmap -x <pid>
Bibliothèques natives
Le code JNI chargé par
System.loadLibrary
peut allouer autant de mémoire hors tas qu'il le souhaite sans aucun contrôle du côté JVM. Cela concerne également la bibliothèque de classes Java standard. En particulier, les ressources Java non fermées peuvent devenir une source de fuite de mémoire native. Des exemples typiques sontZipInputStream
ouDirectoryStream
.Les agents JVMTI, en particulier l'
jdwp
agent de débogage, peuvent également entraîner une consommation excessive de mémoire.Cette réponse décrit comment profiler les allocations de mémoire natives avec async-profiler .
Problèmes d'allocateur
Un processus demande généralement de la mémoire native soit directement du système d'exploitation (par
mmap
appel système), soit en utilisant l'malloc
allocateur libc standard. À son tour,malloc
demande de gros morceaux de mémoire à partir du système d'exploitationmmap
, puis gère ces morceaux en fonction de son propre algorithme d'allocation. Le problème est que cet algorithme peut entraîner une fragmentation et une utilisation excessive de la mémoire virtuelle .jemalloc
, un allocateur alternatif, semble souvent plus intelligent que la libc ordinairemalloc
, donc le passage àjemalloc
peut entraîner une plus petite empreinte gratuite.Conclusion
Il n'existe aucun moyen garanti d'estimer l'utilisation totale de la mémoire d'un processus Java, car il y a trop de facteurs à prendre en compte.
Il est possible de réduire ou de limiter certaines zones de mémoire (comme le cache de code) par des indicateurs JVM, mais beaucoup d'autres ne sont pas du tout sous le contrôle de la JVM.
Une approche possible pour définir les limites de Docker serait de surveiller l'utilisation réelle de la mémoire dans un état «normal» du processus. Il existe des outils et des techniques pour étudier les problèmes de consommation de mémoire Java: Native Memory Tracking , pmap , jemalloc , async-profiler .
Mettre à jour
Voici un enregistrement de ma présentation Empreinte mémoire d'un processus Java .
Dans cette vidéo, je discute de ce qui peut consommer de la mémoire dans un processus Java, comment surveiller et limiter la taille de certaines zones de mémoire et comment profiler les fuites de mémoire native dans une application Java.
la source
https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers/ :
Java voit la taille de la mémoire de l'hôte et ne connaît aucune limitation de la mémoire du conteneur. Cela ne crée pas de pression sur la mémoire, donc GC n'a pas non plus besoin de libérer la mémoire utilisée. J'espère que
XX:MaxRAM
cela vous aidera à réduire l'empreinte mémoire. Finalement, vous pouvez modifier la configuration GC (-XX:MinHeapFreeRatio
,-XX:MaxHeapFreeRatio
, ...)Il existe de nombreux types de métriques de mémoire. Docker semble signaler la taille de la mémoire RSS, qui peut être différente de la mémoire «engagée» rapportée par
jcmd
(les anciennes versions de Docker rapportent le cache RSS + comme utilisation de la mémoire). Bonne discussion et liens: Différence entre Resident Set Size (RSS) et la mémoire totale engagée Java (NMT) pour une JVM exécutée dans un conteneur DockerLa mémoire (RSS) peut également être consommée par d'autres utilitaires du conteneur - shell, gestionnaire de processus, ... Nous ne savons pas ce qui est en cours d'exécution dans le conteneur et comment démarrer les processus dans le conteneur.
la source
-XX:MaxRam
. Je pense qu'il utilise encore plus que le maximum défini mais c'est mieux, merci!-Xmx128m -Xms128m -Xss228k -XX:MaxRAM=256m -XX:+UseSerialGC
, produitDocker 428.5MiB / 600MiB
etjcmd 58 VM.native_memory -> Native Memory Tracking: Total: reserved=1571296KB, committed=314316KB
. JVM prend environ 300 Mo tandis que le conteneur a besoin de 430 Mo. Où sont les 130 Mo entre les rapports JVM et les rapports OS?ps -p 71 -o pcpu,rss,size,vsize
avec le processus Java ayant pid 71. En-XX:MaxRam
fait, cela n'aidait pas, mais le lien que vous avez fourni aide avec le GC série.TL; DR
L'utilisation détaillée de la mémoire est fournie par les détails NMT (Native Memory Tracking) (principalement les métadonnées du code et le garbage collector). En plus de cela, le compilateur et l'optimiseur Java C1 / C2 consomment la mémoire non signalée dans le résumé.
L'encombrement mémoire peut être réduit à l'aide des indicateurs JVM (mais il y a des impacts).
Le dimensionnement du conteneur Docker doit être effectué via des tests avec la charge attendue de l'application.
Détail pour chaque composant
L' espace de classe partagé peut être désactivé à l'intérieur d'un conteneur car les classes ne seront pas partagées par un autre processus JVM. Le drapeau suivant peut être utilisé. Cela supprimera l'espace de classe partagé (17 Mo).
Le garbage collector série du a une empreinte mémoire minimale au prix d'un temps de pause plus long pendant le traitement du ramasse-miettes (voir la comparaison d'Aleksey Shipilëv entre GC sur une image ). Il peut être activé avec l'indicateur suivant. Il peut économiser jusqu'à l'espace GC utilisé (48 Mo).
le compilateur C2 peut être désactivé avec l'indicateur suivant pour réduire les données de profilage utilisées pour décider d'optimiser ou non une méthode.
L'espace de code est réduit de 20 Mo. De plus, la mémoire hors JVM est réduite de 80 Mo (différence entre l'espace NMT et l'espace RSS). Le compilateur d'optimisation C2 a besoin de 100 Mo.
Les compilateurs C1 et C2 peuvent être désactivés avec l'indicateur suivant.
La mémoire en dehors de la JVM est maintenant inférieure à l'espace total engagé. L'espace de code est réduit de 43 Mo. Attention, cela a un impact majeur sur les performances de l'application. La désactivation du compilateur C1 et C2 réduit la mémoire utilisée de 170 Mo.
L'utilisation du compilateur Graal VM (remplacement de C2) conduit à une empreinte mémoire un peu plus petite. Il augmente de 20 Mo l'espace mémoire du code et diminue de 60 Mo de la mémoire JVM extérieure.
L'article Gestion de la mémoire Java pour JVM fournit des informations pertinentes sur les différents espaces mémoire. Oracle fournit quelques détails dans la documentation Native Memory Tracking . Plus de détails sur le niveau de compilation dans la stratégie de compilation avancée et dans disable C2 réduisent la taille du cache de code d'un facteur 5 . Quelques détails sur Pourquoi une JVM signale-t-elle plus de mémoire engagée que la taille de l'ensemble résident du processus Linux? lorsque les deux compilateurs sont désactivés.
la source
Java a besoin de beaucoup de mémoire. La JVM elle-même a besoin de beaucoup de mémoire pour fonctionner. Le tas est la mémoire disponible à l'intérieur de la machine virtuelle, disponible pour votre application. Parce que JVM est un gros paquet contenant tous les bonus possibles, il faut beaucoup de mémoire juste pour le charger.
À partir de java 9, vous avez quelque chose appelé project Jigsaw , qui peut réduire la mémoire utilisée lorsque vous démarrez une application java (avec l'heure de début). Le puzzle de projet et un nouveau système de modules n'ont pas nécessairement été créés pour réduire la mémoire nécessaire, mais si c'est important, vous pouvez essayer.
Vous pouvez consulter cet exemple: https://steveperkins.com/using-java-9-modularization-to-ship-zero-dependency-native-apps/ . En utilisant le système de modules, il en a résulté une application CLI de 21 Mo (avec JRE intégré). JRE prend plus de 200 Mo. Cela devrait se traduire par moins de mémoire allouée lorsque l'application est active (un grand nombre de classes JRE inutilisées ne seront plus chargées).
Voici un autre tutoriel sympa: https://www.baeldung.com/project-jigsaw-java-modularity
Si vous ne voulez pas passer du temps avec cela, vous pouvez simplement allouer plus de mémoire. Parfois c'est le meilleur.
la source
jlink
est assez contraignante car elle nécessitait une modularisation de l'application. Le module automatique n'est pas pris en charge, il n'y a donc pas de moyen facile d'y aller.Comment dimensionner correctement la limite de mémoire Docker? Vérifiez l'application en la surveillant pendant un certain temps. Pour limiter la mémoire du conteneur, essayez d'utiliser l'option -m, --memory bytes pour la commande docker run - ou quelque chose d'équivalent si vous l'exécutez autrement comme
ne peut pas répondre à d'autres questions.
la source