DEV Community

araf
araf

Posted on • Edited on

๐Ÿง  Java Memory Optimization with Guava Cache: A Real-World Guide

๐Ÿง  Java Memory Optimization with Guava Cache: A Real-World Guide

Java is a powerful language, but if you're not careful, it can also be a memory hog. In this post, Iโ€™ll walk you through a real-world scenario where a Java service was running into memory issues โ€” and how we solved it using Guavaโ€™s Cache.

TL;DR: Use bounded caches, close your streams, and profile before you optimize.

๐Ÿšจ The Problem: OutOfMemoryError in Production

We had a Spring Boot service that:

  • Accepted PDF uploads
  • Parsed file content
  • Cached parsed data for reuse

Under heavy load, the app began throwing this:

java.lang.OutOfMemoryError: Java heap space
Enter fullscreen mode Exit fullscreen mode

Letโ€™s dive into what went wrong and how we fixed it.


๐Ÿ”ฌ Step 1: Profile Before You Panic

We used tools like:

  • VisualVM
  • Eclipse Memory Analyzer (MAT)

These revealed:

  • Many unclosed FileInputStreams
  • A massive HashMap<String, byte[]>
  • Large byte[] objects retained indefinitely

๐Ÿ” The Code That Broke Things

Map<String, byte[]> documentCache = new HashMap<>();

public byte[] processFile(String fileName) throws IOException {
    if (documentCache.containsKey(fileName)) {
        return documentCache.get(fileName);
    }

    File file = new File("/docs/" + fileName);
    FileInputStream fis = new FileInputStream(file); // ๐Ÿšจ Leaking stream
    byte[] data = fis.readAllBytes();               // ๐Ÿšจ No close()
    documentCache.put(fileName, data);              // ๐Ÿšจ Unbounded map
    return data;
}
Enter fullscreen mode Exit fullscreen mode

Whatโ€™s wrong?

  • No stream cleanup
  • No eviction policy in the cache
  • Large byte arrays living forever

โœ… The Solution: Guava Cache + Stream Safety

๐Ÿงช Step 1: Close Your Streams!

try (FileInputStream fis = new FileInputStream(file)) {
    byte[] data = fis.readAllBytes();
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Using try-with-resources ensures your streams are closed properly, avoiding file descriptor leaks.


๐Ÿงช Step 2: Add a Bounded Cache

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

Cache<String, byte[]> documentCache = CacheBuilder.newBuilder()
    .maximumSize(100) // Max 100 files cached
    .expireAfterWrite(10, TimeUnit.MINUTES) // Evict after 10 mins
    .build();
Enter fullscreen mode Exit fullscreen mode

This creates a smart cache:

  • ๐Ÿ”„ Automatically evicts unused entries
  • ๐Ÿง  Prevents memory bloat
  • โšก๏ธ Still fast (in-memory)

๐Ÿงช Step 3: Use It Safely

public byte[] processFile(String fileName) throws IOException {
    byte[] cached = documentCache.getIfPresent(fileName);
    if (cached != null) return cached;

    File file = new File("/docs/" + fileName);
    try (FileInputStream fis = new FileInputStream(file)) {
        byte[] data = fis.readAllBytes();
        documentCache.put(fileName, data);
        return data;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now your code:

  • โœ… Frees up memory
  • โœ… Avoids leaks
  • โœ… Stays performant

๐Ÿ”ง JVM Tuning (Bonus)

We also added these JVM flags in Docker:

-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+ExitOnOutOfMemoryError
Enter fullscreen mode Exit fullscreen mode

These help manage memory pressure and auto-restart the service on OOM.


๐Ÿ“ˆ Results

Metric Before After
Memory Usage ~1.4GB ~850MB
Crashes/Day 3โ€“4 0
GC Pause Time High Low

๐Ÿง  Key Takeaways

  • Profile first, donโ€™t blindly optimize.
  • Always close your I/O resources.
  • Use Guavaโ€™s Cache to avoid writing your own LRU logic.
  • Tune your JVM based on the workload.

๐Ÿ™Œ Whatโ€™s Your Java Memory War Story?

Have you ever battled memory leaks or slow GCs? Share your experience in the comments!

If you found this useful, follow me for more real-world Java, Spring Boot, and performance engineering tips!


Top comments (0)