Why could substring() cause a memory leak in older Java versions, and is it still a problem?

Up to Java 7 update 6, substring() didn't copy characters. The new String shared the original's char array and stored only an offset and length. Taking a tiny substring of a huge String (for example a 10-character ID from a 5 MB file) kept the entire 5 MB array alive. Since Java 7u6, substring() copies just the characters it needs, so this leak no longer happens.

Key Points: • Old behaviour: fast O(1) substring, but a small substring could keep a huge array from being garbage collected. • The old workaround was new String(big.substring(0, 10)), which forced a copy. • Modern behaviour: substring() is O(length of the result) and the original can be collected independently. • Java 9 Compact Strings changed the internal storage again (a byte[] plus an encoding flag), but substring still copies. • The general lesson still applies to your own code: views of large data (subList(), ByteBuffer.slice()) keep the whole backing data alive.

Code Example:

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

public class SubstringMemory {

    public static void main(String[] args) {
        List<String> ids = new ArrayList<>();
        Runtime rt = Runtime.getRuntime();

        for (int i = 0; i < 200; i++) {
            String bigLine = "ID" + i + "-" + "x".repeat(1_000_000);   // ~1 MB each
            ids.add(bigLine.substring(0, 6));        // keep only a small ID
        }
        System.gc();
        long usedMb = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);

        System.out.println("Kept " + ids.size() + " ids, first: " + ids.get(0));
        System.out.println("Memory used is far below 200 MB: " + (usedMb < 100));

        // The same trap with a List view: subList keeps the whole list alive
        List<Integer> big = new ArrayList<>();
        for (int i = 0; i < 1_000; i++) big.add(i);
        List<Integer> view = big.subList(0, 3);
        List<Integer> copy = new ArrayList<>(big.subList(0, 3));   // independent copy
        System.out.println("view: " + view + ", copy: " + copy);
    }
}

Output:

Kept 200 ids, first: ID0-xx Memory used is far below 200 MB: true view: [0, 1, 2], copy: [0, 1, 2]

Good to Know: On Java 6, the same loop would keep all 200 large strings in memory (around 400 MB, because chars took two bytes each), even though only 200 short IDs were used. Mentioning the 7u6 change shows you know Java's history, not just its current behaviour.

Interview Tip: A concise interview answer is:

"In Java 6 and early Java 7, substring shared the original char array, so keeping a small substring of a huge string kept the whole array in memory, a classic leak fixed with new String(substring). Since Java 7 update 6, substring copies only the characters it needs, so it's no longer an issue, but the same idea still applies to views like subList."

Previous question: What are Compact Strings in Java 9?

Next question: How do you check whether two strings are anagrams in Java?

More Java Strings interview questions

All Java Strings questions