Mémoire JVM dans Tomcat, Solr et Java

Comment dimensionner le tas, vérifier la configuration et reproduire une fuite de mémoire avec un exercice local.

De combien de mémoire la JVM a-t-elle besoin ?

-Xms fixe la taille initiale du tas et -Xmx sa limite. Les objets vivent dans le tas. Le Metaspace, les piles de threads, les buffers directs et d’autres structures consomment aussi de la mémoire. Gardez une marge sous la RAM disponible ou la limite du conteneur. Pour Solr, prévoyez aussi de la place pour le cache de fichiers de Lucene.

Augmenter le tas de Tomcat

Si vous démarrez avec catalina.sh, modifiez $CATALINA_BASE/bin/setenv.sh. Conservez les options existantes et remplacez les anciennes valeurs de -Xms et -Xmx. CATALINA_OPTS s’applique au processus serveur. Redémarrez par le mécanisme habituel et vérifiez le nouveau processus. Si le service lance Java directement, modifiez sa configuration de démarrage.

CATALINA_OPTS="${CATALINA_OPTS:-} -Xms512m -Xmx2g"

Augmenter le tas de Solr

Sous Linux, modifiez le solr.in.sh lu par le service. Une installation standard en tant que service utilise généralement /etc/default/solr.in.sh. SOLR_HEAP fixe les tailles initiale et maximale. Si SOLR_JAVA_MEM est déjà utilisé, modifiez cette variable pour éviter des réglages concurrents. Redémarrez et vérifiez les valeurs effectives.

SOLR_HEAP="2g"

Ajuster une application Java

Placez les options JVM avant -jar. Les tailles sont des exemples. Avec HotSpot moderne en conteneur, -Xmx fixe la limite. Pour la calculer à partir de la mémoire détectée par la JVM, vous pouvez utiliser -XX:MaxRAMPercentage=60.0 sans définir -Xmx.

java -Xms256m -Xmx1g -jar app.jar

Vérifier le tas et observer le GC

jcmd -l liste les processus Java visibles. Remplacez 1234 par le PID et lancez jcmd sous le compte du service dans son environnement. VM.flags affiche les valeurs effectives, dont MaxHeapSize en octets. GC.heap_info donne un instantané du tas. Sous Linux, ps affiche la RSS en Kio pour l’ensemble du processus.

jcmd -l
jcmd 1234 VM.flags
jcmd 1234 GC.heap_info
ps -p 1234 -o pid,rss,comm

Avec un JDK 11 ou ultérieur, ajoutez cette option au démarrage pour enregistrer les journaux GC avec rotation. Comparez l’occupation après collecte, les pauses et la latence sous la même charge avant et après le changement.

-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M

Quand la mémoire continue d’augmenter

Si l’occupation du tas augmente après les collectes, examinez les références retenues. Si le tas se stabilise mais que la RSS augmente, vérifiez les threads, les buffers et la mémoire native. Activez Native Memory Tracking au démarrage avec -XX:NativeMemoryTracking=summary. Prenez une référence puis comparez après la mise en charge. NMT suit les allocations internes de HotSpot et ne rend pas compte de toute la RSS.

jcmd 1234 VM.native_memory baseline
jcmd 1234 VM.native_memory summary.diff scale=MB

Un court enregistrement JFR aide à repérer les sites d’allocation. Pour savoir ce qui maintient les objets en vie, analysez un vidage du tas. Vous pouvez configurer -XX:+HeapDumpOnOutOfMemoryError et -XX:HeapDumpPath=/chemin/prive avec assez d’espace disque. Un vidage peut contenir des données applicatives et sa création à chaud peut provoquer une pause.

jcmd 1234 JFR.start name=memory settings=profile duration=60s filename=memory.jfr

Exercice local de rétention d’objets

Dans les notes, CustomerManager renvoie le premier client avec get(0), ce qui le laisse dans la liste. Cette version reproduit la différence avec des blocs de 1 Mio. Enregistrez-la sous HeapRetention.java. En mode retain, le programme conserve les références et finit par épuiser un tas de 32 Mio. En mode drain, il retire chaque bloc de la file pour permettre sa collecte.

import java.util.ArrayDeque;
import java.util.Queue;

class HeapRetention {
    public static void main(String[] args) {
        boolean retain = args.length > 0 && args[0].equals("retain");
        Queue<byte[]> pending = new ArrayDeque<>();

        for (int i = 0; i < 96; i++) {
            pending.add(new byte[1024 * 1024]);
            if (retain) {
                pending.element();
            } else {
                pending.remove();
            }
        }
        System.out.println("Pending blocks: " + pending.size());
    }
}

Exécutez les modes séparément. Le premier affiche zéro bloc en attente. Le second provoque volontairement une OutOfMemoryError. Un tas plus grand retarderait l’erreur tant que la file continue de grandir.

java -Xms32m -Xmx32m -Xlog:gc HeapRetention.java drain
java -Xms32m -Xmx32m -Xlog:gc HeapRetention.java retain

Sources