Votre vieille configuration Java ne démarre plus en Java 17 ? La réponse est dans la mémoire de la JVM.

En Java, chaque thread a sa stack, pour ses variables locales. Les objets vivent dans le heap, la zone que le garbage collector nettoie. Les classes sont décrites dans le Metaspace, hors du heap. Et la JVM utilise encore d’autres zones de mémoire, qu’on oublie souvent.

Trois zones à connaître

La mémoire d'un processus Java : les stacks des threads, le heap, le Metaspace et les autres zones nativesProcessus Javatout ce qui est hors du heap est de la mémoire nativeStacksune par thread ·-XssThread maintraiter() · clientmain()Thread 2lire()run()frames, variables localesHeappartagé · géré par le GC ·-XmxClientStringListbyte[]les objets créés avecnewles chaînes internéesles champs statiques des classesMetaspacemémoire nativesans limite par défautClientchamps, méthodes…Stringchamps, méthodes…la description des classes-XX:MaxMetaspaceSizeEt aussi, en mémoire nativeCode cachele code compilé par le JIT240 Mo au maximum par défautDirect memoryByteBuffer.allocateDirect()-XX:MaxDirectMemorySizeStructures internesdu garbage collector,du compilateur…

La stack : une par thread

Chaque thread a sa propre stack. À chaque appel de méthode, la JVM y ajoute une frame : les paramètres et les variables locales de la méthode. Quand la méthode se termine, sa frame disparaît. Pas besoin du garbage collector pour libérer cette mémoire.

Une variable locale de type objet ne contient pas l’objet : elle contient une référence vers un objet du heap.

Si les appels s’empilent trop, par exemple avec une récursion sans fin, la stack déborde : StackOverflowError. Sa taille se règle avec -Xss. La valeur par défaut dépend de la plateforme : 1 Mo sur Linux x64, 2 Mo sur Linux et macOS en ARM (Aarch64).

Le heap : les objets, et le travail du GC

Les objets créés avec new vont dans le heap, partagé par tous les threads. C’est la zone du garbage collector : il cherche les objets que plus rien ne référence, et il récupère leur place.

Sa taille maximale se règle avec -Xmx. Sans cette option, la JVM fixe en général le maximum à 25 % de la mémoire. Sur une machine de 32 Go de mémoire, cela donne un heap maximum de 8 Go.

Le heap contient aussi les chaînes internées (String.intern()) et les champs statiques des classes.

Le Metaspace : les classes, hors du heap

Pour chaque classe chargée, la JVM garde sa description : ses champs, ses méthodes… Ces métadonnées sont dans le Metaspace, en mémoire native, hors du heap.

Par défaut, sa taille n’est pas limitée : il grandit avec le nombre de classes chargées. On peut fixer un maximum avec -XX:MaxMetaspaceSize.

Et ce qu’on oublie souvent

Le heap et le Metaspace ne sont pas les seules zones. La JVM utilise aussi de la mémoire native pour :

  • les stacks des threads : une par thread, donc beaucoup de threads, c’est beaucoup de mémoire ;
  • le code cache : le code machine produit par le compilateur JIT, 240 Mo au maximum par défaut ;
  • la direct memory : les buffers alloués hors du heap avec ByteBuffer.allocateDirect(). Sans -XX:MaxDirectMemorySize, la JVM choisit la limite elle-même ;
  • les structures internes du garbage collector, du compilateur, etc.

Ce qui a changé

Prenons un développeur qui reprend une vieille configuration, trouvée dans un ancien projet ou sur un forum :

java -Xms512m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=256m -jar app.jar

Avec Java 8, l’application démarre. La JVM affiche seulement deux avertissements, faciles à rater dans les logs :

OpenJDK 64-Bit Server VM warning: ignoring option PermSize=128m; support was removed in 8.0
OpenJDK 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0

Avec Java 17, 21 ou 25, elle ne démarre plus :

Unrecognized VM option 'PermSize=128m'
Error: Could not create the Java Virtual Machine.

Et la JVM s’arrête à la première option inconnue : on retire PermSize, on relance, et c’est au tour de MaxPermSize.

La raison : PermGen n’existe plus depuis Java 8.

Ce qu’on lit encoreCe qui est vrai aujourd’huiDepuis
Les classes sont dans PermGen, une zone de taille fixeLeurs métadonnées sont dans le Metaspace, en mémoire native, sans limite par défautJava 8
On règle PermGen avec -XX:PermSize et -XX:MaxPermSizeOptions ignorées avec un avertissement, puis refusées : la JVM ne démarre pas. À la place : -XX:MetaspaceSize et -XX:MaxMetaspaceSizeIgnorées jusqu’à Java 16, refusées depuis Java 17
OutOfMemoryError: PermGen spacePermGen n’existe plus, cette erreur non plusJava 8
Les chaînes internées et les champs statiques sont dans PermGenIls sont dans le heapJava 8 au plus tard

Sources : JEP 122, Remove the Permanent Generation, et la documentation de la commande java de JDK 17 (section « Removed Java Options »).

Deux erreurs courantes

« Tout est dans le heap »

Le heap contient les objets, avec les chaînes internées et les champs statiques. Les classes, le code compilé, les stacks des threads et les buffers directs sont ailleurs, en mémoire native. -Xmx ne les limite pas.

« -Xmx, c’est la mémoire de mon application »

-Xmx ne limite que le heap. Le processus Java utilise toujours plus. Par exemple, avec Java 25 et -Xmx512m, la JVM réserve déjà environ 598 Mo de mémoire utilisable (« committed » dans le Native Memory Tracking), juste pour afficher sa version. Pour dimensionner une machine ou un conteneur, il faut compter tout le processus, pas seulement le heap.

En résumé

Trois zones à retenir : la stack de chaque thread, le heap des objets et le Metaspace des classes. Seul le heap est limité par -Xmx, et il n’est qu’une partie de la mémoire du processus. Si une vieille configuration parle de PermGen, il est temps de la nettoyer.

Pour aller plus loin