Mémoire de la JVM et garbage collector

Publié le · Mis à jour le

L'idée en une phrase

Java range tes objets dans une grande zone mémoire, la heap, et un garbage collector (GC, le « ramasse-miettes ») la nettoie tout seul. Tu n'as jamais à libérer la mémoire toi-même… à condition de ne pas garder, sans le vouloir, des objets dont tu n'as plus besoin.

Une image pour comprendre : l'entrepôt et le bureau

Imagine une entreprise.

  • L'entrepôt (la heap) est partagé par tout le monde. On y stocke les cartons : ce sont tes objets.
  • Chaque employé a un bureau (la stack, une par thread). Il y colle des post-its : « le carton du client Dupont est en allée 4 ». Ce sont tes variables locales et tes références.
  • Quand un employé termine une tâche, il jette ses post-its.
  • Une équipe de ménage (le GC) passe régulièrement dans l'entrepôt. Elle jette tous les cartons que plus aucun post-it ne désigne.

Un carton n'est jamais jeté tant qu'il reste au moins un post-it qui pointe vers lui. Retiens bien ça : c'est l'origine de toutes les fuites mémoire.

Heap et stack dans le code

public CommandeDto afficher(Long id) {
    int quantite = 3;                                   // nombre simple : stack
    Commande commande = repository.findById(id)         // la variable "commande" : stack
            .orElseThrow();                             // l'objet Commande : heap
    String libelle = commande.libelle().toUpperCase();  // nouvelle String : heap
    return new CommandeDto(libelle, quantite);          // nouvel objet : heap
}
// fin de la méthode : quantite, commande et libelle disparaissent de la stack
// l'objet Commande n'est plus désigné par personne → le GC pourra le supprimer
Stack Heap
Contient variables locales, appels de méthodes en cours tous les objets (new ...)
Appartient à un seul thread toute l'application
Libérée automatiquement, à la fin de chaque méthode par le GC, quand plus rien ne pointe vers l'objet
Erreur quand elle est pleine StackOverflowError OutOfMemoryError

Dans une application Spring Boot, c'est rassurant : les objets créés pendant une requête HTTP (entités, DTO, chaînes…) ne sont plus désignés par personne quand la requête se termine. Le GC les supprime. Tu n'as rien à faire.

Le garbage collector

Le GC part des points d'ancrage de l'application : les variables des méthodes en cours d'exécution et les champs static. Il suit toutes les références à partir de là. Tout ce qu'il atteint est conservé ; tout le reste est supprimé.

Ce que tu dois savoir en pratique :

  • Il fonctionne tout seul. Tu n'as pas à l'appeler, et System.gc() n'est qu'une suggestion qui ne règle rien. N'en mets pas dans ton code.
  • Java choisit un GC adapté par défaut (G1). Tu n'as pas à le choisir ni à le régler : c'est un sujet d'exploitation, pas de développement.
  • Il est conçu pour supprimer très efficacement les objets qui vivent peu de temps, comme ceux d'une requête HTTP. Créer des objets n'est pas un problème.

StackOverflowError : la boucle sans fin

La stack déborde quand des méthodes s'appellent les unes les autres sans jamais s'arrêter. Chaque appel ajoute un étage à la pile, jusqu'à ce qu'il n'y ait plus de place.

Le cas d'école, c'est une méthode récursive qui a oublié son cas d'arrêt. Mais en Spring Boot, la cause la plus fréquente est plus sournoise : deux entités qui se référencent mutuellement.

@Entity
public class Client {
    @OneToMany(mappedBy = "client")
    private List<Commande> commandes;   // un client connaît ses commandes
}

@Entity
public class Commande {
    @ManyToOne
    private Client client;              // une commande connaît son client
}
@GetMapping("/clients/{id}")
public Client afficher(@PathVariable Long id) {
    return clientRepository.findById(id).orElseThrow(); // ❌ renvoie l'entité directement
}

Pour transformer le client en JSON, Spring écrit ses commandes, puis le client de chaque commande, puis ses commandes, puis… à l'infini. Résultat : StackOverflowError. Le même piège existe avec un toString() généré (par exemple avec Lombok @Data) sur les deux entités.

La bonne habitude : ne renvoie jamais une entité depuis un contrôleur. Renvoie un DTO, un objet fait pour la réponse, qui ne contient que ce dont le client a besoin.

public record ClientDto(String nom, int nombreCommandes) {}

@GetMapping("/clients/{id}")
public ClientDto afficher(@PathVariable Long id) {
    Client client = clientRepository.findById(id).orElseThrow();
    return new ClientDto(client.getNom(), client.getCommandes().size()); // ✅
}

OutOfMemoryError : la heap est pleine

java.lang.OutOfMemoryError: Java heap space

Cette erreur signifie : « il n'y a plus de place dans l'entrepôt, et le ménage n'a rien trouvé à jeter ». Deux grandes causes dans le code d'un projet Spring Boot.

Cause n°1 : charger trop de données d'un coup

List<Commande> toutes = commandeRepository.findAll(); // ❌ 3 millions de lignes en mémoire

En développement, la table contient 50 lignes et tout va bien. En production, elle en contient des millions : chaque ligne devient un objet dans la heap, et l'application tombe.

Charge les données par pages :

Page<Commande> page = commandeRepository.findAll(PageRequest.of(0, 100)); // ✅ 100 lignes

Ou mieux encore, demande directement à la base ce dont tu as besoin : un comptage, une somme, un filtre. Une requête count ne charge aucune ligne.

long nombre = commandeRepository.countByStatut(Statut.PAYEE); // ✅ rien n'est chargé

Astuce : avant d'écrire findAll(), demande-toi combien de lignes la table contiendra en production, pas sur ton poste.

Cause n°2 : une collection qui grossit sans fin (la fuite mémoire)

Un service Spring existe en un seul exemplaire et vit aussi longtemps que l'application (voir la fiche sur les threads). Tout ce que tu ranges dans un de ses champs est donc gardé pour toujours.

@Service
public class ProduitService {

    private final Map<Long, Produit> cache = new HashMap<>(); // ❌ ne se vide jamais

    public Produit trouver(Long id) {
        return cache.computeIfAbsent(id, cle -> produitRepository.findById(cle).orElseThrow());
    }
}

Chaque produit consulté reste dans la Map, donc reste désigné, donc n'est jamais supprimé par le GC. Au bout de quelques jours, la heap est pleine. C'est une fuite mémoire : pas de bug visible pendant les tests, un plantage en production.

La bonne habitude : ne fabrique pas de cache à la main. Utilise le cache de Spring, avec une taille maximale :

@Cacheable("produits")
public Produit trouver(Long id) {
    return produitRepository.findById(id).orElseThrow();
}

⚠️ Sans configuration, le cache de Spring Boot est lui aussi sans limite. Ajoute la bibliothèque Caffeine (com.github.ben-manes.caffeine:caffeine) et fixe une taille dans application.properties :

spring.cache.caffeine.spec=maximumSize=1000,expireAfterWrite=10m

Le cache garde alors au maximum 1 000 produits, chacun pendant 10 minutes au plus. N'oublie pas @EnableCaching sur une classe de configuration, et garde en tête que @Cacheable passe par un proxy (voir la fiche sur les proxies Spring).

Et augmenter la mémoire ?

Tu entendras parler de -Xmx, l'option qui fixe la taille maximale de la heap (par exemple -Xmx2g). La tentation est grande de l'augmenter au premier OutOfMemoryError.

Si l'erreur vient d'une fuite ou d'un findAll() sur une grosse table, plus de mémoire ne fait que retarder le plantage. Il faut corriger le code.

Le réglage de la mémoire du serveur relève de l'équipe qui déploie. Ton rôle, si tu tombes sur un OutOfMemoryError : relever le message exact et le moment où il est apparu, regarder si le code charge beaucoup de données ou remplit une collection, et en parler à l'équipe.

Les bonnes habitudes

  1. Ne renvoie jamais une entité depuis un contrôleur. Passe par un DTO.
  2. Méfie-toi de findAll(). Pagine, ou demande à la base un comptage, une somme ou un filtre.
  3. Pas de collection qui grossit dans un champ de service. Un service vit toute la durée de l'application.
  4. Pour un cache, utilise @Cacheable avec une taille maximale, jamais une Map faite main.
  5. N'appelle jamais System.gc() et ne touche pas aux réglages du GC.
  6. Face à un OutOfMemoryError, cherche la cause dans le code avant de demander plus de mémoire.

À retenir

  • Les objets vont dans la heap, partagée ; les variables locales vont dans la stack, une par thread.
  • Le GC supprime tout seul les objets que plus rien ne désigne. Ceux d'une requête HTTP sont nettoyés sans que tu aies rien à faire.
  • StackOverflowError : des appels sans fin, souvent deux entités qui se référencent et qu'on transforme en JSON. Solution : un DTO.
  • OutOfMemoryError : trop de données chargées d'un coup, ou une collection qui ne se vide jamais.
  • Augmenter la mémoire ne corrige pas une fuite.

Avis sur l'article

Pas encore de note : soyez le premier à donner votre avis.

Les avis sont publiés après validation par le modérateur.

Votre note