CodeGym /Cours /JAVA 25 SELF /Variables locales, fuites de mémoire, références faibles

Variables locales, fuites de mémoire, références faibles

JAVA 25 SELF
Niveau 64, Leçon 2
Disponible

1. Où sont stockées les variables locales

Commençons par le plus simple : les variables locales. Ce sont des variables déclarées à l’intérieur d’une méthode et qui n’existent que pendant son exécution. Leur vie est courte et dramatique : dès que la méthode se termine, toutes ses variables locales disparaissent sans laisser de trace.

En Java, les variables locales sont stockées dans la pile. Chaque thread possède sa propre pile. Si une méthode en appelle une autre, un nouveau « frame » (stack frame) avec les variables locales et l’adresse de retour est ajouté à la pile. Lorsque la méthode se termine, son frame est retiré de la pile.

Exemple : vie d’une variable locale

public class LocalVariableDemo {
    public static void main(String[] args) {
        int a = 42; // la variable locale a ne vit que dans main
        printSquare(a);
        // Ici, la variable b n'existe déjà plus !
    }

    public static void printSquare(int b) {
        int square = b * b; // variable locale square
        System.out.println("Carré: " + square);
        // Après la sortie de printSquare, toutes les variables locales disparaissent
    }
}

Point important : si une variable locale est une référence à un objet (par exemple, String, Scanner, un tableau), alors la référence elle-même vit dans la pile, tandis que l’objet vit dans le tas ! Lorsque la référence disparaît et qu’il n’existe plus d’autres références vers l’objet, le ramasse-miettes peut supprimer l’objet.

Illustration

Stack (pour main):
| int a = 42      |
| args            |
-------------------
Heap:
| [objets créés via new] |

2. Fuites de mémoire en Java : mythe ou réalité ?

Beaucoup de débutants pensent : « Il y a un ramasse-miettes en Java ! Donc, aucune fuite de mémoire n’est possible ! » Hélas, c’est un mythe qui se dissipe dès le premier grand projet.

Le ramasse-miettes ne supprime que les objets vers lesquels il n’existe aucune référence vivante. S’il reste une référence quelque part (même au plus inattendu des endroits), l’objet restera en mémoire jusqu’au bout. Ou jusqu’à un OutOfMemoryError.

Exemple 1 : collection statique piège

import java.util.ArrayList;
import java.util.List;

public class MemoryLeakDemo {
    // Ah, cette collection statique !
    private static final List<String> BIG_LIST = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            BIG_LIST.add("Ligne numéro " + i);
        }
        System.out.println("Un million de lignes ajoutées");
        // Même si main se termine, BIG_LIST reste en mémoire tant que la JVM vit
    }
}

Que se passe-t-il ?

  • La variable statique BIG_LIST vit aussi longtemps que la classe est chargée (généralement jusqu’à la fin de vie de la JVM).
  • Toutes les chaînes ajoutées à la liste ne peuvent pas être supprimées par le GC — il existe toujours une référence via BIG_LIST.
  • Si vous oubliez d’assainir de telles collections — vous obtiendrez une fuite de mémoire.

Exemple 2 : écouteurs non désinscrits (listeners)

import java.util.ArrayList;
import java.util.List;

class EventSource {
    private final List<Runnable> listeners = new ArrayList<>();

    public void addListener(Runnable listener) {
        listeners.add(listener);
    }

    // ... autres méthodes ...
}

public class ListenerLeakDemo {
    public static void main(String[] args) {
        EventSource source = new EventSource();
        Runnable listener = () -> System.out.println("Événement !");
        source.addListener(listener);
        // Si l'on oublie d'appeler source.removeListener(listener), listener restera en mémoire pour toujours !
    }
}

Problème : si l’écouteur n’est plus nécessaire mais n’est pas retiré de la liste, lui et tous les objets auxquels il fait référence resteront en mémoire.

Exemple 3 : un cache qui n’est jamais purgé

import java.util.HashMap;
import java.util.Map;

public class CacheLeakDemo {
    private static final Map<String, byte[]> CACHE = new HashMap<>();

    public static void main(String[] args) {
        for (int i = 0; i < 1_000_000; i++) {
            // À chaque itération, on crée un tableau de 1 Ko
            CACHE.put("key" + i, new byte[1024]);
        }
        System.out.println("Un million d’éléments ajoutés au cache");
        // Le cache grossit, la mémoire s'épuise, OutOfMemoryError !
    }
}

Conclusion : même avec le GC, on peut facilement obtenir une fuite de mémoire si l’on ne gère pas correctement le cycle de vie des objets !

3. Références faibles (WeakReference) et leurs amis

Parfois, nous avons besoin de caches ou de collections dans lesquels les objets peuvent être supprimés par le GC s’il n’existe plus d’autres références vers eux. Pour cela, on utilise les références faibles (WeakReference).

Références ordinaires (strong)

String s = new String("hello"); // référence forte (strong)

L’objet s vivra en mémoire tant qu’il existe au moins une référence forte.

Référence faible (WeakReference)

import java.lang.ref.WeakReference;

public class WeakRefDemo {
    public static void main(String[] args) {
        String strong = new String("Bonjour, monde !");
        WeakReference<String> weak = new WeakReference<>(strong);

        System.out.println("Avant nettoyage: " + weak.get()); // la référence existe

        strong = null; // on supprime la référence forte

        System.gc(); // on demande au GC de nettoyer la mémoire (non garanti !)

        // Au bout d’un certain temps, weak.get() peut devenir null
        System.out.println("Après GC: " + weak.get());
    }
}

Comment ça marche ?

  • Tant qu’il existe au moins une référence forte vers l’objet, le GC ne le supprimera pas.
  • S’il ne reste que des références faibles, l’objet peut être supprimé lors du prochain passage du ramasse-miettes.
  • La méthode weak.get() renvoie l’objet s’il est encore vivant, ou null s’il a été supprimé.

Où utilise-t-on les références faibles ?

Le principal cas d’usage — les caches, lorsque ce n’est pas critique si l’objet est supprimé de la mémoire. Par exemple, si vous mettez en cache des images mais ne voulez pas que le cache consomme toute la mémoire.

Exemple : WeakHashMap

WeakHashMap est une collection où les clés sont stockées via des références faibles. Si plus personne ne référence la clé, la paire correspondante est supprimée de la map.

import java.util.Map;
import java.util.WeakHashMap;

public class WeakHashMapDemo {
    public static void main(String[] args) {
        Map<Object, String> map = new WeakHashMap<>();
        Object key = new Object();
        map.put(key, "Valeur");

        System.out.println("Avant nettoyage: " + map);

        key = null; // on supprime la référence forte vers la clé

        System.gc(); // on demande au GC de travailler

        // Au bout d’un certain temps, la map devient vide !
        try { Thread.sleep(100); } catch (InterruptedException ignored) {}
        System.out.println("Après GC: " + map);
    }
}

Attention : WeakHashMap ne fonctionne que pour les clés — les valeurs sont stockées avec des références fortes ordinaires.

Soft, Weak, Phantom : toute la famille des références

En Java, il existe quatre types de références (par ordre de « force ») :

Type de référence Quand l’objet est-il supprimé par le GC ? Où s’applique-t-il ?
Strong
Uniquement lorsqu’il n’existe plus aucune référence forte Variables ordinaires, collections
Soft
En cas de manque de mémoire Cache que l’on souhaite garder plus longtemps
Weak
Au prochain passage du GC s’il n’y a plus de références fortes Caches, WeakHashMap, écouteurs
Phantom
Après finalisation, pour suivre la suppression de l’objet Tâches spéciales, nettoyage hors tas
  • SoftReference — l’objet est supprimé en cas de manque de mémoire (idéal pour les caches d’images, etc.).
  • WeakReference — supprimé dès la première collecte si aucune autre référence n’existe.
  • PhantomReference — le type le plus « fantomatique », utile pour des scénarios complexes, rarement utilisé par les débutants.

4. Pratique : exemple de fuite de mémoire et sa correction

Exemple de fuite : liste statique

import java.util.ArrayList;
import java.util.List;

public class LeakExample {
    private static final List<byte[]> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100_000; i++) {
            list.add(new byte[1024 * 1024]); // 1 Mo
            if (i % 10 == 0) System.out.println("Ajouté " + i + " Mo");
        }
    }
}

Que va-t-il se passer ?
Le programme va rapidement consommer toute la mémoire disponible et se terminer par un OutOfMemoryError, car la liste statique conserve des références vers tous les tableaux créés.

Correction : utiliser des références faibles

Si ce n’est pas critique que tous les objets soient toujours disponibles, on peut les stocker via des références faibles :

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;

public class LeakFixed {
    private static final List<WeakReference<byte[]>> list = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100_000; i++) {
            list.add(new WeakReference<>(new byte[1024 * 1024]));
            if (i % 10 == 0) System.out.println("Ajouté " + i + " Mo");
            System.gc(); // Suggestion au GC (ne garantit pas un nettoyage immédiat !)
        }
    }
}

Désormais, les tableaux peuvent être supprimés par le GC s’il n’existe plus d’autres références vers eux — la liste ne contient que des références faibles.

5. Scénarios typiques de fuites de mémoire

  • Écouteurs d’événements : vous avez oublié de retirer l’écouteur — l’objet vit éternellement.
  • Collections statiques : caches non nettoyés, listes globales — tout cela peut conduire à des fuites.
  • Classes internes et lambdas : si une classe interne ou une lambda capture une référence vers l’objet englobant, il ne sera pas supprimé tant que l’objet englobant vit.

Exemple avec une classe interne

public class Outer {
    private byte[] bigArray = new byte[1024 * 1024 * 100]; // 100 Mo

    public Runnable createTask() {
        // La classe interne anonyme capture une référence vers Outer !
        return new Runnable() {
            @Override
            public void run() {
                System.out.println("La tâche s’exécute");
            }
        };
    }

    public static void main(String[] args) {
        Outer outer = new Outer();
        Runnable task = outer.createTask();
        // Même si outer = null, task maintient quand même une référence vers bigArray !
    }
}

Solution : utilisez des classes internes statiques ou externalisez la logique dans des classes séparées afin d’éviter de conserver des références inutiles.

6. Pratique : utilisation de références faibles dans un cache

Ajoutons à une application pédagogique un cache très simple utilisant WeakHashMap.

import java.util.Map;
import java.util.WeakHashMap;

public class ImageCache {
    private final Map<String, byte[]> cache = new WeakHashMap<>();

    public void put(String name, byte[] data) {
        cache.put(name, data);
    }

    public byte[] get(String name) {
        return cache.get(name);
    }

    public static void main(String[] args) {
        ImageCache cache = new ImageCache();
        cache.put("cat", new byte[1024 * 1024]); // 1 Mo
        System.out.println("Le chaton a été ajouté au cache");

        // Si plus aucune référence n'existe vers la clé "cat", l'objet peut être supprimé par le GC
    }
}

Dans les applications réelles (par exemple, des bibliothèques d’images), les références faibles aident à éviter la saturation mémoire en supprimant automatiquement les données rarement utilisées.

7. Strong vs Weak : quand utiliser quoi ?

  • Références fortes (Strong) — par défaut : utilisez-les pour tout ce qui doit vivre de manière garantie.
  • Références faibles — pour les caches, les écouteurs, lorsque ce n’est pas critique si l’objet est supprimé.
  • Références Soft — pour les caches qu’il est souhaitable de conserver plus longtemps, mais supprimables en cas de manque de mémoire.
  • Références Phantom — pour des scénarios avancés (par exemple, finalisation hors heap).

8. Erreurs typiques lors de la gestion de la mémoire et des références

Erreur n° 1 : « Le GC va tout nettoyer pour moi ! » Le ramasse-miettes ne supprime que les objets vers lesquels il n’existe plus aucune référence forte vivante. Si vous « oubliez » une référence quelque part (par exemple, dans une collection static), l’objet vivra éternellement.

Erreur n° 2 : écouteurs oubliés. Vous avez ajouté un écouteur à un objet mais ne l’avez pas retiré lors de la destruction de l’objet ? L’écouteur, ainsi que tout ce qu’il a capturé, restera en mémoire.

Erreur n° 3 : cache sans références faibles. Vous utilisez un HashMap ordinaire pour un cache qui devrait se nettoyer automatiquement ? Mieux vaut utiliser WeakHashMap ou SoftReference.

Erreur n° 4 : classes internes et lambdas qui capturent l’objet englobant. Les classes internes (et les expressions lambda) conservent implicitement une référence vers l’objet englobant. Si vous stockez des instances de telles classes plus longtemps que l’objet englobant lui-même, vous obtiendrez une fuite.

Erreur n° 5 : attendre un déclenchement immédiat du GC. L’appel à System.gc() ne garantit pas qu’une collecte aura lieu immédiatement. Ce n’est qu’une « demande » à la JVM, pas un ordre.

1
Mission
JAVA 25 SELF, niveau 64, leçon 2
Bloqué
Utilisation d'une référence faible – Message sur les encres évanescentes 👻
Utilisation d'une référence faible – Message sur les encres évanescentes 👻
1
Mission
JAVA 25 SELF, niveau 64, leçon 2
Bloqué
Cache utilisant WeakHashMap – Bibliothèque intelligente qui recycle les livres oubliés 📚
Cache utilisant WeakHashMap – Bibliothèque intelligente qui recycle les livres oubliés 📚
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION