Java Memory Management

Java Memory Management is a crucial aspect of the Java Runtime Environment (JRE) that handles memory allocation and deallocation automatically through garbage collection.

1. Java Memory Structure

JVM Memory Areas

Java Memory Structure:
┌─────────────────────────────────────────────────────────────┐
│                    JVM Memory                               │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐  │
│ │   Method    │  │    Heap     │  │     Stack           │  │
│ │   Area      │  │             │  │                     │  │
│ ├─────────────┤  ├─────────────┤  ├─────────────────────┤  │
│ │ Class       │  │  Young Gen  │  │ Thread 1 Stack      │  │
│ │ Structures  │  │   ┌─────┐   │  │ ┌─────────────────┐ │  │
│ │ Runtime     │  │   │Eden │   │  │ │ Stack Frames    │ │  │
│ │ Constant    │  │   └─────┘   │  │ │ Local Variables │ │  │
│ │ Pool        │  │   ┌─────┐   │  │ │ Operand Stack   │ │  │
│ │             │  │   │S0/S1│   │  │ │ Reference to    │ │  │
│ └─────────────┘  │   └─────┘   │  │ │ Heap Objects    │ │  │
│                  │              │  │ └─────────────────┘ │  │
│ ┌─────────────┐  │  Old Gen    │  │ Thread 2 Stack      │  │
│ │  Native     │  │             │  │ ┌─────────────────┐ │  │
│ │  Method     │  │             │  │ │ Stack Frames    │ │  │
│ │  Stack      │  │             │  │ │ ...             │ │  │
│ └─────────────┘  └─────────────┘  └─────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

2. Detailed Memory Areas

Heap Memory

public class HeapMemoryExample {
private static List<String> staticList = new ArrayList<>(); // Class/Heap
private instanceList = new ArrayList<>(); // Instance/Heap
public void demonstrateHeapMemory() {
// All these objects are allocated in Heap
String str = new String("Hello World"); // Heap
List<Integer> numbers = new ArrayList<>(); // Heap
int[] array = new int[1000]; // Heap
// Object references are in stack, objects themselves in heap
Object obj = new Object(); // ref in stack, object in heap
}
}

Stack Memory

public class StackMemoryExample {
public static void main(String[] args) { // main stack frame
int localVar = 10; // Stored in stack
String reference = "Hello"; // reference in stack, object in heap/string pool
methodOne(localVar); // New stack frame created
}
public static void methodOne(int param) { // New stack frame
int localVar = 20; // Stored in this method's stack frame
Object obj = new Object(); // reference in stack, object in heap
methodTwo(); // Another stack frame
} // Stack frame destroyed when method returns
public static void methodTwo() {
// Each method call creates a new stack frame with:
// - Local variables
// - Method parameters  
// - Return address
// - Operand stack
}
}

Method Area

public class MethodAreaExample {
// Class-level data stored in Method Area
private static final String CLASS_CONSTANT = "CONSTANT_VALUE";
private static int staticCounter = 0;
// Class metadata, method code, constant pool, field data
// are all stored in Method Area
}

3. Garbage Collection

How Garbage Collection Works

public class GarbageCollectionExample {
public static void main(String[] args) {
demonstrateGC();
System.gc(); // Suggest JVM to run GC (not guaranteed)
Runtime.getRuntime().gc(); // Alternative way
}
public static void demonstrateGC() {
// Object becomes eligible for GC when no references point to it
Object obj1 = new Object(); // Object created
Object obj2 = new Object();
obj1 = obj2; // Original obj1 object becomes unreachable
// The object that was referenced by obj1 is now eligible for GC
obj2 = null; // Now both original objects are unreachable
// Method local objects become eligible after method execution
createTemporaryObjects();
}
public static void createTemporaryObjects() {
for(int i = 0; i < 1000; i++) {
String temp = new String("Temp " + i);
// Each temp becomes eligible for GC after loop iteration
}
// All temp objects are eligible for GC after method returns
}
}

Object Lifecycle and GC Eligibility

public class ObjectLifecycle {
private static List<Object> staticList = new ArrayList<>();
public static void main(String[] args) {
// Case 1: Nullifying reference
Object obj1 = new Object();
obj1 = null; // Eligible for GC
// Case 2: Reassigning reference
Object obj2 = new Object();
Object obj3 = new Object();
obj2 = obj3; // Original obj2 object eligible for GC
// Case 3: Isolated islands
Object island1 = new Object();
Object island2 = new Object();
island1 = island2;
island2 = island1;
// Both objects reference each other but no external references
// Eligible for GC (island of isolation)
// Case 4: Method local objects
createShortLivedObjects();
// Case 5: Static references - objects stay in memory
staticList.add(new Object()); // Will not be GC'd while staticList exists
}
public static void createShortLivedObjects() {
Object shortLived = new Object();
// shortLived becomes eligible for GC after method returns
}
}

4. Generational Garbage Collection

Heap Generations

public class GenerationalGCExample {
public static void main(String[] args) {
demonstrateObjectAging();
}
public static void demonstrateObjectAging() {
// Young Generation (Eden + Survivor Spaces)
List<Object> youngObjects = new ArrayList<>();
// Most objects die young
for (int i = 0; i < 10000; i++) {
Object shortLived = new Object();
if (i % 100 != 0) {
// Most objects become unreachable immediately
youngObjects.add(shortLived);
}
}
youngObjects.clear(); // All objects become eligible for GC
// Objects that survive multiple GC cycles move to Old Generation
List<Object> longLivedObjects = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object longLived = new Object();
longLivedObjects.add(longLived);
}
// These objects will survive multiple GC cycles and eventually
// be promoted to Old Generation
}
}

5. Memory Management Best Practices

1. Avoid Memory Leaks

public class MemoryLeakPrevention {
private static final Map<Object, Object> CACHE = new HashMap<>();
private static final List<Object> STATIC_LIST = new ArrayList<>();
// ❌ Potential memory leak - objects never removed from cache
public void addToCacheBad(Object key, Object value) {
CACHE.put(key, value);
}
// ✅ Better approach - use WeakHashMap or size limits
private static final Map<Object, Object> WEAK_CACHE = new WeakHashMap<>();
private static final int MAX_CACHE_SIZE = 1000;
private static final Map<Object, Object> LIMITED_CACHE = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_CACHE_SIZE;
}
};
public void addToCacheGood(Object key, Object value) {
LIMITED_CACHE.put(key, value);
}
// ❌ Static collections holding object references
public void registerObjectBad(Object obj) {
STATIC_LIST.add(obj); // Object can never be GC'd
}
// ✅ Use weak references for listeners/callbacks
private static final List<WeakReference<Object>> WEAK_LIST = new ArrayList<>();
public void registerObjectGood(Object obj) {
WEAK_LIST.add(new WeakReference<>(obj));
}
}

2. Efficient Object Creation

public class EfficientObjectCreation {
// ❌ Inefficient - creates new object every time
public String createStringBad() {
return new String("constant"); // Creates new object each time
}
// ✅ Better - uses string pool
public String createStringGood() {
return "constant"; // Reuses from string pool
}
// ❌ Creates unnecessary temporary objects
public String concatenateBad(String[] parts) {
String result = "";
for (String part : parts) {
result += part; // Creates new String object each iteration
}
return result;
}
// ✅ Uses StringBuilder to avoid temporary objects
public String concatenateGood(String[] parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb.append(part);
}
return sb.toString();
}
// Object pooling for expensive objects
private static final Queue<ExpensiveObject> OBJECT_POOL = new LinkedList<>();
public ExpensiveObject getExpensiveObject() {
ExpensiveObject obj = OBJECT_POOL.poll();
if (obj == null) {
obj = new ExpensiveObject();
}
return obj;
}
public void returnExpensiveObject(ExpensiveObject obj) {
obj.reset(); // Reset object state
OBJECT_POOL.offer(obj);
}
}
class ExpensiveObject {
private byte[] largeData = new byte[1024 * 1024]; // 1MB
public void reset() {
// Reset object to initial state
Arrays.fill(largeData, (byte) 0);
}
}

3. Proper Resource Management

public class ResourceManagement {
// ❌ Resource leak - stream not closed
public void readFileBad(String filename) {
try {
FileInputStream fis = new FileInputStream(filename);
// read file...
// fis never closed!
} catch (IOException e) {
e.printStackTrace();
}
}
// ✅ Traditional try-finally
public void readFileBetter(String filename) {
FileInputStream fis = null;
try {
fis = new FileInputStream(filename);
// read file...
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// ✅ Best - try-with-resources (Java 7+)
public void readFileBest(String filename) {
try (FileInputStream fis = new FileInputStream(filename);
BufferedInputStream bis = new BufferedInputStream(fis)) {
// read file...
// Resources automatically closed
} catch (IOException e) {
e.printStackTrace();
}
}
// For custom resources
public void useCustomResource() {
try (MyResource resource = new MyResource()) {
resource.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// Custom resource implementing AutoCloseable
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("Doing something...");
}
@Override
public void close() throws Exception {
System.out.println("Cleaning up resources...");
// Release native resources, close connections, etc.
}
}

6. Monitoring and Diagnostics

Memory Monitoring Tools

public class MemoryMonitoring {
public static void monitorMemory() {
Runtime runtime = Runtime.getRuntime();
// Memory statistics
long maxMemory = runtime.maxMemory(); // Maximum heap size
long totalMemory = runtime.totalMemory(); // Current heap size
long freeMemory = runtime.freeMemory(); // Free memory in heap
long usedMemory = totalMemory - freeMemory; // Actually used memory
System.out.println("=== Memory Statistics ===");
System.out.println("Max Memory: " + (maxMemory / (1024 * 1024)) + " MB");
System.out.println("Total Memory: " + (totalMemory / (1024 * 1024)) + " MB");
System.out.println("Free Memory: " + (freeMemory / (1024 * 1024)) + " MB");
System.out.println("Used Memory: " + (usedMemory / (1024 * 1024)) + " MB");
// Memory usage percentage
double usagePercentage = (double) usedMemory / totalMemory * 100;
System.out.println("Memory Usage: " + String.format("%.2f", usagePercentage) + "%");
}
public static void forceGarbageCollection() {
System.out.println("\nBefore GC:");
monitorMemory();
// Suggest garbage collection
System.gc();
// Wait a bit for GC to complete
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("\nAfter GC:");
monitorMemory();
}
public static void createMemoryPressure() {
List<byte[]> memoryHog = new ArrayList<>();
try {
// Allocate memory until we approach limits
while (true) {
byte[] largeArray = new byte[10 * 1024 * 1024]; // 10MB
memoryHog.add(largeArray);
System.out.println("Allocated: " + (memoryHog.size() * 10) + "MB");
// Check memory usage
Runtime runtime = Runtime.getRuntime();
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
long maxMemory = runtime.maxMemory();
if (usedMemory > maxMemory * 0.8) {
System.out.println("Approaching memory limit, stopping...");
break;
}
Thread.sleep(100);
}
} catch (OutOfMemoryError e) {
System.out.println("OutOfMemoryError caught!");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
memoryHog.clear();
System.gc();
}
}
public static void main(String[] args) {
monitorMemory();
forceGarbageCollection();
// createMemoryPressure(); // Use with caution!
}
}

7. Common Memory Issues and Solutions

Memory Leaks

public class CommonMemoryLeaks {
// 1. Static Fields Holding Object References
private static final Map<String, Object> LEAKY_CACHE = new HashMap<>();
public void addToLeakyCache(String key, Object value) {
LEAKY_CACHE.put(key, value); // Objects never removed
}
// Solution: Use soft/weak references or implement cleanup
private static final Map<String, SoftReference<Object>> SAFE_CACHE = new HashMap<>();
// 2. Unclosed Resources
public void leakResources() {
// Connections, streams, etc. that are never closed
// Solution: Use try-with-resources
try (Connection conn = DriverManager.getConnection("url");
PreparedStatement stmt = conn.prepareStatement("SQL")) {
// work with resources
} catch (SQLException e) {
e.printStackTrace();
}
}
// 3. Listeners and Callbacks Not Removed
private List<EventListener> listeners = new ArrayList<>();
public void addListener(EventListener listener) {
listeners.add(listener);
}
// Forgot to remove listeners - objects can't be GC'd
// Solution: Always provide removeListener method
public void removeListener(EventListener listener) {
listeners.remove(listener);
}
// 4. Inner Classes Holding Outer Class References
public class LeakyInnerClass {
private byte[] data = new byte[1024 * 1024];
// Inner class implicitly holds reference to outer class
// This prevents outer class from being GC'd if inner class is referenced
}
// Solution: Use static nested class when outer reference not needed
public static class NonLeakyNestedClass {
private byte[] data = new byte[1024 * 1024];
// No implicit reference to outer class
}
}

Handling Large Objects

public class LargeObjectManagement {
// For very large objects, consider off-heap storage
public void handleLargeData() {
// ❌ Large on-heap allocation
// byte[] hugeArray = new byte[500 * 1024 * 1024]; // 500MB
// ✅ Consider memory-mapped files for large data
try (FileChannel channel = FileChannel.open(
Paths.get("largefile.dat"),
StandardOpenOption.READ, 
StandardOpenOption.WRITE,
StandardOpenOption.CREATE)) {
MappedByteBuffer mappedBuffer = channel.map(
FileChannel.MapMode.READ_WRITE, 0, 500 * 1024 * 1024);
// Work with memory-mapped data
// This uses OS virtual memory instead of Java heap
} catch (IOException e) {
e.printStackTrace();
}
}
// Lazy initialization for expensive objects
private volatile ExpensiveObject expensiveInstance;
public ExpensiveObject getExpensiveObject() {
if (expensiveInstance == null) {
synchronized (this) {
if (expensiveInstance == null) {
expensiveInstance = new ExpensiveObject();
}
}
}
return expensiveInstance;
}
}

8. JVM Memory Options

Common JVM Memory Flags

public class JVMMemoryOptions {
/*
Common JVM Memory Arguments:
-Xms<size>        Set initial Java heap size
-Xmx<size>        Set maximum Java heap size
-Xss<size>        Set java thread stack size
-XX:NewSize=<size> Set initial young generation size
-XX:MaxNewSize=<size> Set maximum young generation size
-XX:NewRatio=<ratio> Set ratio between young and old generation
-XX:SurvivorRatio=<ratio> Set ratio between eden and survivor spaces
Examples:
-Xms512m -Xmx2g -Xss256k
-XX:NewRatio=2 -XX:SurvivorRatio=8
Monitoring:
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
-Xlog:gc* - for Java 9+ unified logging
*/
public static void printRecommendedSettings() {
System.out.println("""
Recommended JVM Memory Settings for Production:
Server Applications:
-Xms2g -Xmx2g (Start with equal min and max to avoid resizing)
-Xss256k (Reduce thread stack size for more threads)
-XX:+UseG1GC (Use G1 garbage collector)
Memory-Intensive Applications:
-Xms4g -Xmx8g (Larger heap for big data processing)
-XX:NewRatio=1 (More young generation space)
-XX:MaxGCPauseMillis=200 (Control GC pause times)
Default GC Settings by Java Version:
Java 8: Parallel GC
Java 11+: G1 GC
""");
}
}

9. Practical Memory Optimization

Real-World Optimization Example

public class MemoryOptimizedCache<K, V> {
private final Map<K, SoftReference<V>> cache;
private final int maxSize;
private final LinkedHashMap<K, Long> accessOrder;
public MemoryOptimizedCache(int maxSize) {
this.maxSize = maxSize;
this.cache = new HashMap<>();
this.accessOrder = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, Long> eldest) {
return size() > maxSize;
}
};
}
public void put(K key, V value) {
cache.put(key, new SoftReference<>(value));
accessOrder.put(key, System.currentTimeMillis());
cleanup();
}
public V get(K key) {
SoftReference<V> ref = cache.get(key);
if (ref != null) {
V value = ref.get();
if (value != null) {
accessOrder.put(key, System.currentTimeMillis());
return value;
} else {
// Reference was cleared by GC, remove from cache
cache.remove(key);
accessOrder.remove(key);
}
}
return null;
}
private void cleanup() {
// Remove entries where soft reference was cleared
cache.entrySet().removeIf(entry -> 
entry.getValue().get() == null);
}
public int size() {
cleanup();
return cache.size();
}
}

Summary

Key Points for Java Memory Management:

  1. Heap vs Stack: Objects in heap, references and primitives in stack
  2. Garbage Collection: Automatic memory reclamation, generational approach
  3. Memory Areas: Heap, Stack, Method Area, Native Method Stack
  4. Common Issues: Memory leaks, resource leaks, large object handling
  5. Best Practices:
  • Use try-with-resources
  • Avoid memory leaks in caches and listeners
  • Properly manage object lifecycle
  • Monitor memory usage
  • Choose appropriate JVM settings

Tools for Memory Analysis:

  • VisualVM
  • JConsole
  • Java Mission Control
  • Eclipse MAT (Memory Analyzer Tool)
  • jstat command-line tool

Effective memory management is crucial for building scalable, high-performance Java applications. Understanding these concepts helps in writing efficient code and troubleshooting memory-related issues.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper