Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
107: Understanding OutOfMemoryError Types
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:
- Analyze the provided code and identify why increasing the heap size is a suboptimal solution.
- Refactor the
processOrdersmethod to use aStream-based approach or a temporary file/database-backed deduplication strategy that ensures the memory usage remains constant regardless of the number of orders. - 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);
}
}There are no comments for now.