Leaks in a collected language

JVM · lesson 10 of 34 · 4 min read

A collector cannot free what your code still points at. That is every Java memory leak, in one line.

Open this lesson in the learning hub

Key points

  • A Java leak is not lost memory. It is memory you are still holding a reference to, by accident.
  • The usual culprit is a static collection that only ever grows. Static means root, and root means immortal.
  • Unbounded caches do the same thing. Bound them by size or by age, or use a cache library that already does.
  • Listeners and callbacks that get registered and never removed drag whole object graphs along with them.
  • A ThreadLocal on a pooled thread outlives the request that set it. Clear it in a finally block, every time.
  • The signature is old gen creeping upwards after every full GC. Take two heap dumps an hour apart and compare what grew.

Example

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

public class Main {

    static final List<byte[]> CACHE = new ArrayList<>();   // static, never evicted: the classic leak

    public static void main(String[] args) {
        System.out.println("used at start   : " + usedMb() + " MB");

        for (int i = 0; i < 200; i++) { CACHE.add(new byte[100 * 1024]); }   // ~20 MB
        System.gc();
        System.out.println("after 200 puts  : " + usedMb() + " MB  <- GC cannot touch it");

        CACHE.clear();
        System.gc();
        System.out.println("after clear()   : " + usedMb() + " MB  <- now unreachable, freed");
        System.out.println();
        System.out.println("Nothing was lost. The code simply kept pointing at it.");
    }

    static long usedMb() {
        Runtime rt = Runtime.getRuntime();
        return (rt.totalMemory() - rt.freeMemory()) >> 20;
    }
}

The collector is not leaking. Your reference is.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the JVM course, and every lesson in it is listed on the JVM contents page.