Mémoire Java

Cette page a été rédigée il y a fort fort longtemps, et n'a pas tellement été mise à jour.

 

Vous savez, moi je ne crois pas qu'il y ait de bonne ou de mauvaise page. Moi, si je devais résumer mon wiki aujourd'hui avec vous, je dirais que c'est d'abord des rencontres. Des gens qui m'ont tendu la main, peut-être à un moment où je ne pouvais pas, où j'étais seul chez moi. Et c'est assez curieux de se dire que les hasards, les rencontres forgent une destinée... Parce que quand on a le goût de la chose, quand on a le goût de la chose bien faite, le beau geste, parfois on ne trouve pas l'interlocuteur en face je dirais, le miroir qui vous aide à avancer. Alors ça n'est pas mon cas, comme je disais là, puisque moi au contraire, j'ai pu ; et je dis merci au wiki, je lui dis merci, je chante le wiki, je danse le wiki... je ne suis qu'amour ! Et finalement, quand des gens me disent « Mais comment fais-tu pour avoir cette humanité ? », je leur réponds très simplement que c'est ce goût de l'amour, ce goût donc qui m'a poussé aujourd'hui à entreprendre une construction logicielle... mais demain qui sait ? Peut-être simplement à me mettre au service de la communauté, à faire le don, le don de soi.

La mémoire utilisée par un processus Java est organisée en plusieurs parties :

  • Heap : elle sert à l’instanciation des objets

  • Perm : on parle de l’espace des objets permanent, pour la définition des classes

  • Code cache : elle est utilisée pour le code machine généré par le compilateur JIT

  • Stack : chaque thread a son espace d’exécution

La taille totale du processus peut être calculé ainsi : mémoire totale = heap + perm + code cache + (stack size x nb threads) + …​

Heap

TODO

Perm

TODO

Code cache

TODO

Stack

La taille du stack doit être suffisamment grande pour permettre l’exécution de méthodes complexes. Un stack size trop faible peut déboucher sur une erreur de type "java.lang.StackOverflowError". Attention toutefois, cette erreur peut être tout simplement due à une récursivité infinie, ou trop profonde. A l’opposé, un stack size trop grand peut déboucher sur une erreur de type "java.lang.OutOfMemoryError".

Les arguments qui permettent de modifier la taille du stack sont -Xss ou -XX:ThreadStackSize. Le rôle de chacun de ces arguments est relativement mal défini, et peut changer d’un système à l’autre.

# Utilisation de -Xss : il faut préciser l'unité
java -Xss512k MaClasse

# Utilisation de -XX:ThreadStackSize : en ko
java -XX:ThreadStackSize=512 MaClasse

La détermination du stack n’est pas toujours simple. Lorsqu’on consulte la documentation de Sun sur le sujet, on se rend compte que plusieurs paramètres peuvent entrer en ligne de compte, en particulier le système d’exploitation. Par exemple, sous Linux, il semblerait qu’il faille parfois modifier la taille de stack des threads au niveau du système.

 ulimit -s 2024

Quelques petits tests permettent de constater concrètement comment ces paramètres sont pris en compte. Le premier est une simple méthode recursive, qui s’appelle elle-même jusqu’à ce qu’un erreur java.lang.StackOverflowError survienne. Le programme intercepte l’erreur et affiche le nombre d’appels qui ont été effectués.

Lorsque je réalise ce test sous Windows 32bits, avec un JDK 6 de Sun, avec une machine de profil client (mono-processeur), j’obtiens les résultats suivants :

java EvaluateStack recursive
=> 8506 calls

java -server EvaluateStack recursive
=> 5811 calls

java -Xss320k EvaluateStack recursive
=> 8506 calls

java -server -Xss320k EvaluateStack recursive
=> 5811 calls

java  -server -Xss512k EvaluateStack recursive
=> 9592 calls

java -server -Xss320k EvaluateStack recursive
=> 14650 calls

java -XX:ThreadStackSize=512 EvaluateStack recursive
=> 8506 calls

java -server -XX:ThreadStackSize=512 EvaluateStack recursive
=> 5811 calls

java -Xss4096k EvaluateStack recursive
=> 129340 calls

La première conclusion, c’est que la valeur par défaut du stack size est de 320ko, et que Hotspot client permet une récursivité plus importante. La seconde, c’est que l’argument -XX:ThreadStackSize semble inopérant. Une telle réponse n’étant pas satisfaisante, j’ai poussé le test en modifiant mon code. Dans la première version l’appel récursif était directement appelé depuis la méthode main, c’est-à-dire dans le thread principal ; après modification, l’appel récursif se fait dans un thread autonome, ce qui change significativement les résultats ;

java -XX:ThreadStackSize=512 EvaluateStack recursive
=> 14636 calls

java -Xss512k EvaluateStack recursive
=> 14636 calls

La nouvelle conclusion, c’est que -Xss affecte tous les threads, y compris le principal, et -XX:ThreadStackSize ne concerne pas le thread principal. Il semblerait que certaines versions de la JVM aient un argument spécifique pour celui-ci (-XX:MainThreadStackSize), ce qui n’est pas le cas pour mon environnement.

Les mêmes tests réalisés sur la même machine, mais avec un JDK 5 donne les résultats suivants :

java EvaluateStack recursive
=> 6620 calls

java -Xss120k EvaluateStack recursive
=> 6620 calls

java -Xss256k EvaluateStack recursive
=> 31196 calls

java -Xss1024k EvaluateStack recursive
=> 31196 calls

Par conséquent, les appels récursifs en JDK 5 semblent beaucoup moins gourmands ! Par contre, la valeur prise en compte semble stagner entre 256k et 1024k (même nombre de calls) ; au-delà de 1024k, les deux versions du JDK présentent les mêmes valeurs de calls. Autre différence notable, -Xss ne semble pas affecter le thread principal.

Enfin, dans un dernier test, j’ai évalué le nombre de threads que la JVM pouvait supporter avant d’émettre un java.lang.OutOfMemoryError. Pour cela, je démarre des threads en boucle, et chaque thread est mis en attente. Sans surpise, le nombre de threads supportés décroit lorsqu’on augmente le stack size.

java -Xss320k EvaluateStack threads
=> Threads created = 5657

java -Xss1024k EvaluateStack threads
=> Threads created = 1806

Il est intéressant de constater que ce nombre décroit lorsque la taille réservée au heap augmente. Ceci s’exmplique par le fait que les threads occupent la mémoire entre les espaces réservés (heap, perm, code cache,…​) et la taille maximale du processus.

java -Xmx512m -Xss1024k EvaluateStack threads
=> Threads created = 1358

Le même test sous Linux Ubuntu 32bits; avec OpenJDK 6 nous donne des résultats très similaires en ce qui concerne la récursivité. En revanche, mon test de nombre de threads ne fonctionne pas, on arrive à un blocage de la machine (machine virtuelle avec peu de capacité) !

Récapitulatif

Pour spécifier la taille de chaque zone, on utilise les paramètres de lancement de Java suivant :

Espace Maximum Initial

Heap

-Xmx

-Xms

Perm

-XX:MaxPermSize

-XX:PermSize

Code cache

-XX:ReservedCodeCacheSize

-XX:InitialCodeCacheSize

Stack

-Xss ou -XX:ThreadStackSize

(taille fixe)

Une liste exhaustive des paramètres de la machine virtuelle de Sun peut être trouvée sur le site de HP (!) ou celui de Sun, avec une page pour les paramètres standards et une page pour les paramètres spécifiques.