Skip to Content
Course content

107: Understanding OutOfMemoryError Types

Click on the "Edit" button in the top corner of the screen to edit your slide content.

The first time I hit a java.lang.OutOfMemoryError (OOM), my immediate reaction was exactly what yours probably is: "I just need to give the JVM more memory." It feels like a simple resource problem. If the program crashes because it ran out of space, the logical solution is to increase the -Xmx flag and call it a day. But here is the thing—treating every OOM as a "not enough RAM" problem is a dangerous habit that often masks deeper architectural flaws or memory leaks that will eventually crash your production server regardless of how many gigabytes you throw at it.

The Instinct to Just Add More RAM

Imagine you're writing a service that imports a massive product catalog from a CSV file. The naive approach is to read every line of that file and add it to a List<Product> before processing them in bulk. On your local machine with a small test file, it works perfectly. In staging, with a 100MB file, it works. Then it hits production with a 2GB file, and suddenly you see java.lang.OutOfMemoryError: Java heap space.

// The naive approach: loading everything into memory
List<Product> products = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader("catalog.csv"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        products.add(parseProduct(line)); // This list grows until the heap explodes
    }
}
processProducts(products);

If you just increase the heap size from 2GB to 8GB, the code might work for a while. But you haven't fixed the problem; you've just moved the goalposts. You're now vulnerable to a larger file, or multiple concurrent requests that each try to load a large file. This is the classic "Java heap space" error—the JVM simply cannot find a contiguous block of memory to allocate your next object.

Listening to What the JVM is Actually Telling You

To fix this properly, you have to look at the type of OOM. When you see GC overhead limit exceeded, the JVM is telling you something different than a standard heap error. It's saying, "I'm spending 98% of my time doing garbage collection, but I'm recovering less than 2% of the heap." In other words, the JVM is thrashing. It's desperately trying to clear space to keep the app alive, but it can't. Increasing the RAM here often just makes the "death spiral" take longer to happen, rather than preventing it.

The better way to handle the catalog import isn't a bigger heap, but a streaming approach. By processing each product one by one (or in small batches) and letting the GC reclaim the memory from the previous product, your memory footprint remains constant regardless of whether the file is 1MB or 1TB.

// The professional approach: Streaming the data
try (BufferedReader reader = new BufferedReader(new FileReader("catalog.csv"))) {
    reader.lines()
          .map(this::parseProduct)
          .forEach(this::processProduct); // Memory is reclaimed as we go
}

When it's Not the Heap's Fault

Sometimes you'll see java.lang.OutOfMemoryError: Metaspace. This is where the "just add more RAM" mentality really fails because -Xmx doesn't affect Metaspace. Metaspace is where the JVM stores class definitions. You don't get this from loading too many Product objects; you get this from loading too many Product.class definitions.

I've seen this happen in projects that make heavy use of reflection or dynamic proxy generation (like some older versions of Hibernate or Spring) where classes are being generated on the fly and never unloaded. If you see a Metaspace error, stop looking at your data structures and start looking at your classloader. You might have a leak in your framework configuration or a library that is creating classes in a loop.

Finally, there's the dreaded Requested array size exceeds VM limit. This is the most honest OOM because it has nothing to do with how much memory you have. The JVM has a hard limit on how large a single array can be (usually slightly less than Integer.MAX_VALUE). If you try to allocate an array larger than that, it will crash instantly. No amount of -Xmx will save you here; you simply have to break your data into multiple arrays or use a different data structure entirely.




📋 Practical Task

Debug and Fix the Catalog Thrashing Leak

You have been handed a legacy OrderProcessor class that is triggering java.lang.OutOfMemoryError: GC overhead limit exceeded when processing large batches of orders. The current implementation loads all orders into a HashMap to "deduplicate" them before saving them to the database.

Your task:

  1. Analyze the provided code and identify why increasing the heap size is a suboptimal solution.
  2. Refactor the processOrders method to use a Stream-based approach or a temporary file/database-backed deduplication strategy that ensures the memory usage remains constant regardless of the number of orders.
  3. Verify that your solution no longer stores the entire dataset in a collection before processing.
public class OrderProcessor {
    public void processOrders(List<String> orderIds) {
        // This is the culprit: loading everything into a Map for deduplication
        Map<String, Order> uniqueOrders = new HashMap<>();
        for (String id : orderIds) {
            Order order = database.fetchOrder(id); 
            uniqueOrders.put(order.getId(), order);
        }
        
        uniqueOrders.values().forEach(this::saveToArchive);
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.