Cela a été une plainte de longue date avec Java, mais cela n'a en grande partie aucun sens et est généralement basé sur l'examen d'informations erronées. La formulation habituelle est quelque chose comme "Hello World sur Java prend 10 mégaoctets ! Pourquoi en a-t-il besoin ?" Eh bien, voici un moyen de faire en sorte que Hello World sur une JVM 64 bits prétende prendre plus de 4 gigaoctets ... au moins par une forme de mesure.
java -Xms1024m -Xmx4096m com.example.Hello
Différentes façons de mesurer la mémoire
Sous Linux, la commande top vous donne plusieurs nombres différents pour la mémoire. Voici ce qu'il dit à propos de l'exemple 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 est l'espace de mémoire virtuelle :la somme de tout dans la carte de mémoire virtuelle (voir ci-dessous). Cela n'a pratiquement aucun sens, sauf quand ce n'est pas le cas (voir ci-dessous).
- RES est la taille de l'ensemble résident :le nombre de pages qui résident actuellement dans la RAM. Dans presque tous les cas, c'est le seul chiffre que vous devez utiliser pour dire "trop gros". Mais ce n'est toujours pas un très bon chiffre, surtout quand on parle de Java.
- SHR est la quantité de mémoire résidente partagée avec d'autres processus. Pour un processus Java, cela se limite généralement aux bibliothèques partagées et aux fichiers JAR mappés en mémoire. Dans cet exemple, je n'avais qu'un seul processus Java en cours d'exécution, donc je soupçonne que le 7k est le résultat des bibliothèques utilisées par le système d'exploitation.
- SWAP n'est pas activé par défaut et n'est pas affiché ici. Il indique la quantité de mémoire virtuelle qui réside actuellement sur le disque, si elle se trouve ou non dans l'espace d'échange . Le système d'exploitation est très bon pour conserver les pages actives dans la RAM, et les seuls remèdes à l'échange sont (1) acheter plus de mémoire, ou (2) réduire le nombre de processus, il est donc préférable d'ignorer ce nombre.
La situation pour le Gestionnaire des tâches de Windows est un peu plus compliquée. Sous Windows XP, il existe des colonnes "Memory Usage" et "Virtual Memory Size", mais la documentation officielle ne dit rien sur leur signification. Windows Vista et Windows 7 ajoutent plus de colonnes, et elles sont en fait documentées. Parmi celles-ci, la mesure "Working Set" est la plus utile ; cela correspond à peu près à la somme de RES et SHR sous Linux.
Comprendre la carte de la mémoire virtuelle
La mémoire virtuelle consommée par un processus est le total de tout ce qui se trouve dans la carte mémoire du processus. Cela inclut les données (par exemple, le tas Java), mais également toutes les bibliothèques partagées et les fichiers mappés en mémoire utilisés par le programme. Sous Linux, vous pouvez utiliser la commande pmap pour voir toutes les choses mappées dans l'espace de processus (à partir de maintenant, je ne ferai référence qu'à Linux, car c'est ce que j'utilise ; je suis sûr qu'il existe des outils équivalents pour Les fenêtres). Voici un extrait de la carte mémoire du programme "Hello World" ; la carte mémoire complète compte plus de 100 lignes et il n'est pas rare d'avoir une liste de mille lignes.
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 ...
Une explication rapide du format :chaque ligne commence par l'adresse de mémoire virtuelle du segment. Viennent ensuite la taille du segment, les autorisations et la source du segment. Ce dernier élément est soit un fichier soit "anon", qui indique un bloc de mémoire alloué via mmap.
En partant du haut, nous avons
- Le chargeur JVM (c'est-à-dire le programme qui s'exécute lorsque vous tapez
java
). C'est très petit; tout ce qu'il fait est de charger dans les bibliothèques partagées où le vrai code JVM est stocké. - Un tas de blocs anon contenant le tas Java et les données internes. Il s'agit d'une JVM Sun, donc le tas est divisé en plusieurs générations, chacune étant son propre bloc de mémoire. Notez que la JVM alloue de l'espace de mémoire virtuelle en fonction du
-Xmx
évaluer; cela lui permet d'avoir un tas contigu. Le-Xms
La valeur est utilisée en interne pour indiquer la quantité de tas "utilisée" au démarrage du programme et pour déclencher la récupération de place lorsque cette limite est proche. - Un fichier JAR mappé en mémoire, dans ce cas le fichier qui contient les "classes JDK". Lorsque vous mappez en mémoire un JAR, vous pouvez accéder très efficacement aux fichiers qu'il contient (au lieu de le lire depuis le début à chaque fois). La JVM Sun mappera en mémoire tous les fichiers JAR sur le chemin de classe ; si votre code d'application doit accéder à un JAR, vous pouvez également le mapper en mémoire.
- Données par thread pour deux threads. Le bloc 1M est la pile de threads. Je n'avais pas de bonne explication pour le bloc 4k, mais @ericsoe l'a identifié comme un "bloc de garde":il n'a pas d'autorisations de lecture/écriture, il provoquera donc une erreur de segment en cas d'accès, et la JVM l'attrape et traduit à un
StackOverFlowError
. Pour une vraie application, vous verrez des dizaines, voire des centaines de ces entrées répétées dans la carte mémoire. - L'une des bibliothèques partagées contenant le code JVM réel. Il y en a plusieurs.
- La bibliothèque partagée pour la bibliothèque standard C. Ce n'est qu'une des nombreuses choses que la JVM charge et qui ne font pas strictement partie de Java.
Les bibliothèques partagées sont particulièrement intéressantes :chaque bibliothèque partagée a au moins deux segments :un segment en lecture seule contenant le code de la bibliothèque, et un segment en lecture-écriture qui contient des données globales par processus pour la bibliothèque (je ne sais pas ce que segment sans autorisations est ; je ne l'ai vu que sur Linux x64). La partie en lecture seule de la bibliothèque peut être partagée entre tous les processus qui utilisent la bibliothèque ; par exemple, libc
dispose de 1,5 M d'espace de mémoire virtuelle pouvant être partagé.
Quand la taille de la mémoire virtuelle est-elle importante ?
La carte de la mémoire virtuelle contient beaucoup de choses. Une partie est en lecture seule, une partie est partagée et une partie est allouée mais jamais touchée (par exemple, la quasi-totalité des 4 Go de tas dans cet exemple). Mais le système d'exploitation est suffisamment intelligent pour ne charger que ce dont il a besoin, de sorte que la taille de la mémoire virtuelle n'est en grande partie pas pertinente.
Là où la taille de la mémoire virtuelle est importante, c'est si vous utilisez un système d'exploitation 32 bits, où vous ne pouvez allouer que 2 Go (ou, dans certains cas, 3 Go) d'espace d'adressage de processus. Dans ce cas, vous avez affaire à une ressource rare et vous devrez peut-être faire des compromis, comme réduire la taille de votre segment de mémoire afin de mapper en mémoire un fichier volumineux ou de créer de nombreux threads.
Mais, étant donné que les machines 64 bits sont omniprésentes, je ne pense pas qu'il faudra longtemps avant que la taille de la mémoire virtuelle ne devienne une statistique totalement hors de propos.
Quand la taille de l'ensemble résident est-elle importante ?
La taille de l'ensemble résident est la partie de l'espace de mémoire virtuelle qui se trouve réellement dans la RAM. Si votre RSS devient une partie importante de votre mémoire physique totale, il est peut-être temps de commencer à vous inquiéter. Si votre RSS grossit jusqu'à occuper toute votre mémoire physique et que votre système commence à échanger, il est plus que temps de commencer à vous inquiéter.
Mais RSS est également trompeur, surtout sur une machine peu chargée. Le système d'exploitation ne déploie pas beaucoup d'efforts pour récupérer les pages utilisées par un processus. Il y a peu d'avantages à en tirer, et le potentiel d'un défaut de page coûteux si le processus touche la page à l'avenir. Par conséquent, la statistique RSS peut inclure de nombreuses pages qui ne sont pas utilisées activement.
Conclusion
Sauf si vous échangez, ne vous inquiétez pas trop de ce que vous disent les diverses statistiques de mémoire. Avec la mise en garde qu'un flux RSS sans cesse croissant peut indiquer une sorte de fuite de mémoire.
Avec un programme Java, il est beaucoup plus important de prêter attention à ce qui se passe dans le tas. La quantité totale d'espace consommé est importante et vous pouvez prendre certaines mesures pour la réduire. Le plus important est le temps que vous consacrez à la collecte des ordures et les parties du tas qui sont collectées.
L'accès au disque (c'est-à-dire à une base de données) coûte cher et la mémoire est bon marché. Si vous pouvez échanger l'un contre l'autre, faites-le.
La quantité de mémoire allouée au processus Java correspond à peu près à ce à quoi je m'attendais. J'ai eu des problèmes similaires en exécutant Java sur des systèmes embarqués/à mémoire limitée. Exécuter tout l'application avec des limites arbitraires de VM ou sur des systèmes qui n'ont pas des quantités adéquates d'échange ont tendance à se casser. Cela semble être la nature de nombreuses applications modernes qui ne sont pas conçues pour être utilisées sur des systèmes à ressources limitées.
Vous avez quelques options supplémentaires que vous pouvez essayer et limiter l'empreinte mémoire de votre JVM. Cela pourrait réduire l'encombrement de la mémoire virtuelle :
-XX:ReservedCodeCacheSize=32m Taille du cache de code réservé (en octets) - taille maximale du cache de code. [Solaris 64 bits, amd64 et -server x86 :48 m ; in1.5.0_06 et versions antérieures, Solaris 64 bits et and64 :1024m.]
-XX:MaxPermSize=64m Taille de la génération permanente. [5.0 et versions ultérieures :les machines virtuelles 64 bits sont agrandies de 30 % ; 1.4amd64 : 96 m ; 1.3.1 -client :32m.]
En outre, vous devez également définir votre -Xmx (taille maximale du tas) sur une valeur aussi proche que possible de l'utilisation maximale réelle de la mémoire de votre candidature. Je crois que le comportement par défaut de la JVM est toujours de doubler la taille du tas chaque fois qu'il l'étend jusqu'au maximum. Si vous commencez avec un tas de 32 M et que votre application atteint 65 M, le tas finira par croître de 32 M > 64 M -> 128 M.
Vous pouvez également essayer ceci pour rendre la VM moins agressive dans la croissance du tas :
-XX:MinHeapFreeRatio=40 Pourcentage minimum de tas libre après GC pour éviter l'expansion.
De plus, d'après ce que je me souviens d'avoir expérimenté cela il y a quelques années, le nombre de bibliothèques natives chargées avait un impact énorme sur l'empreinte minimale. Le chargement de java.net.Socket a ajouté plus de 15 Mo si je me souviens bien (et je ne le fais probablement pas).
Il y a un problème connu avec Java et glibc>=2.10 (inclut Ubuntu>=10.04, RHEL>=6).
Le remède est de fixer cet env. variables :
export MALLOC_ARENA_MAX=4
Si vous utilisez Tomcat, vous pouvez l'ajouter à TOMCAT_HOME/bin/setenv.sh
fichier.
Pour Docker, ajoutez ceci à Dockerfile
ENV MALLOC_ARENA_MAX=4
Il existe un article IBM sur la configuration 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
Ce billet de blog dit
la mémoire résidente est connue pour s'infiltrer d'une manière similaire à une fuite de mémoire ou à une fragmentation de la mémoire.
Il existe également un bogue JDK ouvert JDK-8193521 "glibc gaspille de la mémoire avec la configuration par défaut"
recherchez MALLOC_ARENA_MAX sur Google ou SO pour plus de références.
Vous voudrez peut-être également régler d'autres options de malloc pour optimiser une faible fragmentation de la mémoire allouée :
# 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