Cache Design
1. Estimate Cache Memory needed for caching 1 million entries of a Java object
About
In a typical Spring Boot microservice, suppose we are caching frequently accessed data using an in-memory cache like ConcurrentHashMap, Caffeine, or Spring’s built-in caching abstraction. Each cached entry consists of:
Key: A unique identifier (e.g., String or UUID)
Value: A Java object with approximately 10–15 fields
Let’s estimate how much JVM heap memory will be required to cache 1 million such objects.
Breakdown of Memory Usage
1. Object Overhead (JVM Internal Structure)
Every object in the JVM has some built-in memory overhead, regardless of the fields it contains. This includes:
Object Header
12 bytes (on 64-bit JVM with compressed oops)
Stores object metadata like identity hash, GC info, and type pointer.
Object Padding
Up to 8 bytes (to align to 8-byte boundaries)
JVM aligns object size to 8-byte multiples for efficient memory access.
Reference Fields
4 or 8 bytes each
Depending on whether compressed oops are enabled.
Internal Pointers
Adds indirection and slight overhead when objects contain references to other objects.
2. Estimated Field Composition (Example: 10 Fields)
Let's assume the object being cached looks like this:
public class UserProfile {
private UUID id;
private String name;
private String email;
private int age;
private boolean active;
private long createdAt;
private String country;
private String phone;
private int loginCount;
private boolean verified;
// Total: 10 fields
}UUID
1
16 bytes
Backed by two long values
String
4
40–80 bytes each
Depends on string length; average 60 bytes
int
2
4 bytes each
Total: 8 bytes
boolean
2
1 byte each (but rounded up)
Total padded to 4–8 bytes
long
1
8 bytes
64-bit long
Total estimated size of fields:
UUID= 16 bytes4 Strings= ~240 bytes2 int= 8 bytes2 boolean(padded) = ~8 bytes1 long= 8 bytes
→ Total = ~280–320 bytes for fields
3. Additional Memory Per Entry (Cache-Level Overhead)
When storing these objects in a map or cache (e.g., ConcurrentHashMap or Caffeine), we also need to account for:
Key Object
~40–60 bytes
UUID or String, including its internal character array
Map Entry Overhead
~32–48 bytes
Bucket pointer, hash, references
Value Object
~300–350 bytes
As estimated above
References
~8–16 bytes
Reference to value and key
Total per cache entry: ~400–500 bytes conservatively In worst cases, may grow up to 550–600 bytes.
4. Total Estimated Memory Usage
To calculate total memory for 1 million entries:
Per Entry (average): 500 bytes
Total Entries : 1,000,000
-------------------------------
Total Memory Needed : 500 * 1,000,000 = 500,000,000 bytes ≈ 476 MBBuffering for Safety
Due to GC metadata, alignment, fragmentation, and fluctuations in field size:
Add 30–40% buffer
Recommended Heap Size: 700–800 MB
5. How to Measure Actual Memory Usage (Empirical Approach) ?
To validate the estimate with real data:
Write a test class to load 1 million objects into a
Map:Map<UUID, UserProfile> cache = new HashMap<>(); for (int i = 0; i < 1_000_000; i++) { cache.put(UUID.randomUUID(), new UserProfile(...)); }Use a Java profiler:
JVisualVM: Attach to the JVM and inspect heap usage.
Eclipse MAT (Memory Analyzer Tool): For analyzing heap dumps.
YourKit or JProfiler: For in-depth memory profiling.
Compare memory usage before and after population.
2. LRU Cache
Design and build a "least recently used" cache, which evicts the least recently used item. The cache should map from keys to values (allowing us to insert and retrieve a value associated with a particular key) and be initialized with a max size. When it is full, it should evict the least recently used item.
We need to design a cache that:
Stores key-value pairs.
Has a maximum size (capacity).
When inserting a new item and the cache is full, it must evict the least recently used (LRU) item.
Both
get(key)andput(key, value)operations must be done in O(1) time.
To support O(1) operations:
Use a HashMap to store key → node mappings.
Use a Doubly Linked List to track usage order (most recently used at head, least at tail).
Whenever:
We
get(key): Move the node to the front (MRU).We
put(key, value):If key exists: Update and move to front.
If key doesn’t exist and at capacity: Remove tail (LRU), insert new node at head.
import java.util.HashMap;
import java.util.Map;
public class LRUCache {
// Doubly linked list node
private class Node {
int key, value;
Node prev, next;
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
private final int capacity;
private final Map<Integer, Node> cache; // Maps keys to nodes
private final Node head, tail; // Dummy head and tail for ease of insertion/removal
public LRUCache(int capacity) {
this.capacity = capacity;
this.cache = new HashMap<>();
// Initialize dummy head and tail to avoid null checks
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
// Get the value associated with a key
public int get(int key) {
if (!cache.containsKey(key)) {
return -1; // Not found
}
Node node = cache.get(key);
// Move the accessed node to the front (most recently used)
moveToFront(node);
return node.value;
}
// Put a key-value pair into the cache
public void put(int key, int value) {
if (cache.containsKey(key)) {
// Key exists: update value and move to front
Node node = cache.get(key);
node.value = value;
moveToFront(node);
} else {
// New key
if (cache.size() == capacity) {
// Remove least recently used (LRU) node from tail
Node lru = tail.prev;
removeNode(lru);
cache.remove(lru.key);
}
Node newNode = new Node(key, value);
addToFront(newNode);
cache.put(key, newNode);
}
}
// Move an existing node to the front (MRU position)
private void moveToFront(Node node) {
removeNode(node);
addToFront(node);
}
// Add a node right after dummy head
private void addToFront(Node node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
// Remove a node from the list
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}public class Main {
public static void main(String[] args) {
LRUCache cache = new LRUCache(2);
cache.put(1, 10); // cache = {1=10}
cache.put(2, 20); // cache = {2=20, 1=10}
System.out.println(cache.get(1)); // returns 10, cache = {1=10, 2=20}
cache.put(3, 30); // evicts key 2, cache = {3=30, 1=10}
System.out.println(cache.get(2)); // returns -1
cache.put(4, 40); // evicts key 1, cache = {4=40, 3=30}
System.out.println(cache.get(1)); // returns -1
System.out.println(cache.get(3)); // returns 30
System.out.println(cache.get(4)); // returns 40
}
}Last updated