Mémoire Java


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

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.