Java's heap memory allocation is managed by the Garbage Collector (GC), but understanding allocation strategies helps write memory-efficient applications and optimize performance.
1. Heap Memory Structure
Generational Heap Layout
┌─────────────────────────────────────────────────────────────┐ │ Java Heap Memory │ ├─────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Young │ │ Old │ │ Permanent/Meta │ │ │ │ Generation │ │ Generation │ │ Space │ │ │ │ │ │ │ │ │ │ │ ├─────────────┤ ├─────────────┤ ├─────────────────────┤ │ │ │ Eden │ │ │ │ Class metadata, │ │ │ │ │ │ Tenured │ │ interned strings, │ │ │ ├─────────────┤ │ │ │ runtime constants │ │ │ │ Survivor │ │ Space │ │ │ │ │ │ 0 │ │ │ │ │ │ │ ├─────────────┤ │ │ │ │ │ │ │ Survivor │ │ │ │ │ │ │ │ 1 │ │ │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
Heap Regions Demonstration
public class HeapStructureDemo { public static void main(String[] args) { demonstrateGenerationalAllocation(); showHeapStatistics(); demonstrateObjectAges(); } public static void demonstrateGenerationalAllocation() { System.out.println("=== Generational Heap Allocation ==="); // Small, short-lived objects go to Eden space for (int i = 0; i < 5; i++) { byte[] shortLived = new byte[1024]; // 1KB - likely in Young Gen System.out.println("Created short-lived object: " + shortLived.hashCode()); } // Large object might go directly to Old Gen byte[] largeObject = new byte[10 * 1024 * 1024]; // 10MB - might go to Old Gen System.out.println("Created large object: " + largeObject.hashCode()); demonstrateSurvivorPromotion(); } public static void demonstrateSurvivorPromotion() { System.out.println("\n=== Survivor Space Promotion ==="); List<byte[]> longLivedObjects = new ArrayList<>(); // Create objects that survive multiple GC cycles for (int i = 0; i < 100; i++) { byte[] object = new byte[2048]; // 2KB objects // Keep references to prevent GC if (i % 10 == 0) { longLivedObjects.add(object); System.out.println("Keeping object from iteration: " + i); } // Force minor GC occasionally (not recommended in production) if (i % 30 == 0) { System.gc(); // Hint to GC - for demonstration only } } System.out.println("Long-lived objects count: " + longLivedObjects.size()); } public static void showHeapStatistics() { System.out.println("\n=== Heap Statistics ==="); Runtime runtime = Runtime.getRuntime(); long maxMemory = runtime.maxMemory(); long totalMemory = runtime.totalMemory(); long freeMemory = runtime.freeMemory(); long usedMemory = totalMemory - freeMemory; System.out.printf("Max Heap: %,d MB%n", maxMemory / (1024 * 1024)); System.out.printf("Current Heap: %,d MB%n", totalMemory / (1024 * 1024)); System.out.printf("Used Heap: %,d MB%n", usedMemory / (1024 * 1024)); System.out.printf("Free Heap: %,d MB%n", freeMemory / (1024 * 1024)); // Calculate utilization percentage double utilization = (double) usedMemory / totalMemory * 100; System.out.printf("Heap Utilization: %.2f%%%n", utilization); } public static void demonstrateObjectAges() { System.out.println("\n=== Object Age Demonstration ==="); // Objects have an "age" counter in the header // After surviving certain number of GC cycles, they get promoted to Old Gen List<Object> agingObjects = new ArrayList<>(); for (int age = 1; age <= 15; age++) { Object obj = new Object(); agingObjects.add(obj); System.out.printf("Object age group %d: %s%n", age, obj.hashCode()); // In real JVM, age increases when object survives GC // MaxTenuringThreshold determines promotion age (default: 15) } System.out.println("MaxTenuringThreshold typically defaults to 15 GC cycles"); } } 2. Object Allocation Strategies
TLAB (Thread-Local Allocation Buffer)
public class TLABAllocationDemo { private static final int OBJECT_COUNT = 10000; private static final int OBJECT_SIZE = 256; // bytes public static void main(String[] args) throws InterruptedException { demonstrateTLABAllocation(); compareAllocationStrategies(); } public static void demonstrateTLABAllocation() { System.out.println("=== TLAB (Thread-Local Allocation Buffer) ==="); System.out.println(""" TLAB Strategy: - Each thread gets a private allocation buffer - Objects allocated pointer-bump style within TLAB - No synchronization needed for allocation - When TLAB fills, thread requests new TLAB """); // Multi-threaded allocation demonstration int threadCount = 4; ExecutorService executor = Executors.newFixedThreadPool(threadCount); CountDownLatch latch = new CountDownLatch(threadCount); long startTime = System.currentTimeMillis(); for (int i = 0; i < threadCount; i++) { final int threadId = i; executor.submit(() -> { allocateObjectsInThread(threadId); latch.countDown(); }); } latch.await(); long endTime = System.currentTimeMillis(); System.out.printf("Multi-threaded allocation completed in %d ms%n", endTime - startTime); executor.shutdown(); } private static void allocateObjectsInThread(int threadId) { List<byte[]> threadLocalObjects = new ArrayList<>(); for (int i = 0; i < OBJECT_COUNT / 4; i++) { // Each thread allocates objects in its TLAB byte[] obj = new byte[OBJECT_SIZE]; threadLocalObjects.add(obj); if (i % 1000 == 0) { System.out.printf("Thread %d allocated object %d%n", threadId, i); } } System.out.printf("Thread %d completed: %d objects%n", threadId, threadLocalObjects.size()); } public static void compareAllocationStrategies() { System.out.println("\n=== Allocation Strategy Comparison ==="); System.out.println(""" Allocation Strategies: 1. TLAB (Thread-Local Allocation Buffer) - Default - Pros: No synchronization, very fast - Cons: Small memory overhead per thread 2. Non-TLAB Allocation - Pros: No TLAB overhead - Cons: Requires global synchronization 3. Large Object Allocation - Objects > TLAB size go directly to Old Gen - Avoids copying in Young GC """); // Demonstrate large object allocation demonstrateLargeObjectAllocation(); } public static void demonstrateLargeObjectAllocation() { System.out.println("\n=== Large Object Allocation ==="); // TLAB size is typically 1-2% of Eden space // Objects larger than TLAB go directly to Old Generation int[] smallObject = new int[100]; // Likely in TLAB/Young Gen int[] mediumObject = new int[1000]; // Might fit in TLAB int[] largeObject = new int[100000]; // Likely goes directly to Old Gen System.out.println("Small object array length: " + smallObject.length); System.out.println("Medium object array length: " + mediumObject.length); System.out.println("Large object array length: " + largeObject.length); System.out.println(""" Large Object Allocation Behavior: - Objects larger than TLAB size bypass Young Generation - Allocated directly in Old Generation - Avoids copying overhead during Young GC - Can lead to faster Old Gen fragmentation """); } } Escape Analysis and Stack Allocation
public class EscapeAnalysisDemo { public static void main(String[] args) { demonstrateEscapeAnalysis(); demonstrateStackAllocation(); compareAllocationPerformance(); } public static void demonstrateEscapeAnalysis() { System.out.println("=== Escape Analysis ==="); System.out.println(""" Escape Analysis determines if an object: NoEscape: Object doesn't escape the method → stack allocation possible ArgEscape: Object passed as argument but doesn't escape thread GlobalEscape: Object escapes the method → must be heap allocated JVM can eliminate allocation for NoEscape objects. """); // Example 1: No escape - potential stack allocation int result1 = calculateSum(10, 20); System.out.println("NoEscape example result: " + result1); // Example 2: Method escape - heap allocation required Point escapedPoint = createAndEscapePoint(5, 10); System.out.println("Escaped point: " + escapedPoint); } // NoEscape example - Point doesn't escape the method private static int calculateSum(int x, int y) { Point localPoint = new Point(x, y); // Might be stack allocated return localPoint.x + localPoint.y; } // ArgEscape example - Object escapes but only to calling method private static Point createAndEscapePoint(int x, int y) { return new Point(x, y); // Escapes to caller } // GlobalEscape example - Object stored in static field private static Point globalPoint; private static void storeInStaticField(int x, int y) { globalPoint = new Point(x, y); // Global escape } public static void demonstrateStackAllocation() { System.out.println("\n=== Stack Allocation Opportunities ==="); // Scalar replacement - object fields become local variables for (int i = 0; i < 1000; i++) { // This Rectangle might be replaced with primitive fields Rectangle rect = new Rectangle(i, i * 2, 10, 20); int area = rect.getArea(); if (i % 100 == 0) { System.out.printf("Iteration %d, area: %d%n", i, area); } } System.out.println(""" Stack Allocation Benefits: - No heap allocation overhead - Automatic cleanup when method exits - Better cache locality - Reduced GC pressure """); } public static void compareAllocationPerformance() { System.out.println("\n=== Allocation Performance Comparison ==="); int iterations = 100000; // With potential stack allocation long startTime = System.nanoTime(); long stackAllocSum = testStackAllocation(iterations); long stackTime = System.nanoTime() - startTime; // With forced heap allocation startTime = System.nanoTime(); long heapAllocSum = testHeapAllocation(iterations); long heapTime = System.nanoTime() - startTime; System.out.printf("Stack-style allocation: %,d ns%n", stackTime); System.out.printf("Heap allocation: %,d ns%n", heapTime); System.out.printf("Performance ratio: %.2fx%n", (double) heapTime / stackTime); System.out.println("Sum verification: " + (stackAllocSum == heapAllocSum)); } private static long testStackAllocation(int iterations) { long sum = 0; for (int i = 0; i < iterations; i++) { // Object might be stack allocated or scalar replaced Point p = new Point(i, i * 2); sum += p.x + p.y; } return sum; } private static long testHeapAllocation(int iterations) { List<Point> points = new ArrayList<>(iterations); long sum = 0; for (int i = 0; i < iterations; i++) { Point p = new Point(i, i * 2); points.add(p); // Force heap allocation sum += p.x + p.y; } return sum; } static class Point { final int x, y; Point(int x, int y) { this.x = x; this.y = y; } @Override public String toString() { return String.format("Point(%d, %d)", x, y); } } static class Rectangle { final int x, y, width, height; Rectangle(int x, int y, int width, int height) { this.x = x; this.y = y; this.width = width; this.height = height; } int getArea() { return width * height; } } } 3. Garbage Collection Impact on Allocation
GC-Aware Allocation Strategies
public class GCAllocationStrategies { public static void main(String[] args) throws Exception { demonstrateGCImpactOnAllocation(); demonstrateAllocationFailureHandling(); showGCTriggeringPatterns(); } public static void demonstrateGCImpactOnAllocation() { System.out.println("=== GC Impact on Allocation ==="); System.out.println(""" GC Events Affect Allocation: Young GC (Minor GC): - Clears Eden and Survivor spaces - Promotes long-lived objects to Old Gen - Resets TLABs and allocation pointers Full GC (Major GC): - Clears entire heap (Young + Old Gen) - Stops application threads (Stop-the-World) - Significant performance impact """); // Monitor allocation rate around GC monitorAllocationRate(); } public static void monitorAllocationRate() { System.out.println("\n=== Allocation Rate Monitoring ==="); long initialFreeMemory = Runtime.getRuntime().freeMemory(); List<byte[]> allocatedObjects = new ArrayList<>(); // Allocate objects and track rate long startTime = System.currentTimeMillis(); int allocationCount = 0; for (int i = 0; i < 100000; i++) { byte[] obj = new byte[1024]; // 1KB objects allocatedObjects.add(obj); allocationCount++; // Check memory and GC impact periodically if (i % 10000 == 0) { long currentFreeMemory = Runtime.getRuntime().freeMemory(); long allocatedMemory = initialFreeMemory - currentFreeMemory; System.out.printf("After %,d allocations: allocated %,d KB%n", i, allocatedMemory / 1024); // Hint GC (for demonstration only) if (i % 30000 == 0) { System.gc(); System.out.println("GC requested..."); } } } long endTime = System.currentTimeMillis(); long duration = endTime - startTime; double allocationRate = (double) allocationCount / duration * 1000; System.out.printf("Allocation rate: %.2f objects/second%n", allocationRate); } public static void demonstrateAllocationFailureHandling() { System.out.println("\n=== Allocation Failure Handling ==="); System.out.println(""" Allocation Failure Scenarios: 1. Eden Space Full → Trigger Young GC 2. Young GC cannot free enough space → Promote to Old Gen 3. Old Gen Full → Trigger Full GC 4. Full GC cannot free space → OutOfMemoryError """); try { // Simulate allocation patterns that trigger different GC behaviors simulateAllocationPatterns(); } catch (OutOfMemoryError e) { System.err.println("OutOfMemoryError caught: " + e.getMessage()); } } private static void simulateAllocationPatterns() { System.out.println("Simulating different allocation patterns..."); // Pattern 1: Short-lived objects (good for Young Gen) createShortLivedObjects(); // Pattern 2: Long-lived objects (fill Old Gen) createLongLivedObjects(); // Pattern 3: Mixed lifespan objects (realistic scenario) createMixedLifespanObjects(); } private static void createShortLivedObjects() { System.out.println("Creating short-lived objects..."); for (int i = 0; i < 10000; i++) { byte[] shortLived = new byte[2048]; // 2KB // No reference kept - immediately eligible for GC } } private static List<byte[]> longLivedStorage = new ArrayList<>(); private static void createLongLivedObjects() { System.out.println("Creating long-lived objects..."); for (int i = 0; i < 1000; i++) { byte[] longLived = new byte[10240]; // 10KB longLivedStorage.add(longLived); // Keep reference } } private static void createMixedLifespanObjects() { System.out.println("Creating mixed lifespan objects..."); Random random = new Random(); List<byte[]> mixedObjects = new ArrayList<>(); for (int i = 0; i < 5000; i++) { byte[] obj = new byte[1024 + random.nextInt(4096)]; // 1-5KB // 20% chance to keep object (simulate long-lived) if (random.nextDouble() < 0.2) { mixedObjects.add(obj); } } System.out.println("Kept " + mixedObjects.size() + " long-lived objects"); } public static void showGCTriggeringPatterns() { System.out.println("\n=== GC Triggering Patterns ==="); System.out.println(""" Common GC Trigger Scenarios: Allocation Failure: - Eden space full when trying to allocate System.gc() Call: - Explicit GC request (avoid in production) Old Generation Full: - Promotion failure from Young GC Metadata Space Full: - Too many classes loaded G1 GC Humongous Allocation: - Very large objects (>50% region size) """); } } 4. Memory Pool-Specific Strategies
Young Generation Allocation
public class YoungGenAllocation { public static void main(String[] args) { demonstrateEdenAllocation(); demonstrateSurvivorBehavior(); showYoungGenOptimizations(); } public static void demonstrateEdenAllocation() { System.out.println("=== Eden Space Allocation ==="); System.out.println(""" Eden Allocation Characteristics: - Most new objects allocated here - Pointer-bump allocation in TLABs - Very fast allocation (just pointer increment) - Cleared completely during Young GC """); // Demonstrate rapid Eden allocation List<byte[]> edenObjects = new ArrayList<>(); long edenAllocationStart = System.currentTimeMillis(); for (int i = 0; i < 50000; i++) { byte[] obj = new byte[512]; // Small objects in Eden edenObjects.add(obj); } long edenAllocationTime = System.currentTimeMillis() - edenAllocationStart; System.out.printf("Allocated %,d Eden objects in %d ms%n", edenObjects.size(), edenAllocationTime); // Clear most references to allow GC edenObjects.clear(); } public static void demonstrateSurvivorBehavior() { System.out.println("\n=== Survivor Space Behavior ==="); System.out.println(""" Survivor Space Role: - Hold objects that survive Young GC - Two spaces (S0, S1) for copying - Objects copied between survivors each GC - Age counter increments on each survival - Promoted to Old Gen after MaxTenuringThreshold """); // Create objects that will survive multiple GC cycles List<byte[]> survivorCandidates = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < 1000; i++) { byte[] obj = new byte[1024]; survivorCandidates.add(obj); // Simulate different object lifetimes if (random.nextDouble() < 0.3) { // These will likely be promoted } } System.out.println("Created " + survivorCandidates.size() + " potential survivor objects"); demonstrateAgeBasedPromotion(); } public static void demonstrateAgeBasedPromotion() { System.out.println("\n=== Age-Based Promotion ==="); // Objects have age counter in header // After surviving certain GC cycles, promoted to Old Gen System.out.println(""" Age Tracking: - Each object header contains age field (4 bits) - Age increments when object survives Young GC - MaxTenuringThreshold controls promotion age - Default: 15 GC cycles Adaptive Sizing: - JVM can adjust tenuring threshold dynamically - Based on survivor space utilization - Prevents premature promotion """); } public static void showYoungGenOptimizations() { System.out.println("\n=== Young Generation Optimizations ==="); System.out.println(""" Optimization Strategies: 1. Object Reuse: - Reuse objects instead of creating new ones - Reduces allocation rate 2. Primitive Arrays: - Use int[] instead of Integer[] - Reduces object header overhead 3. Object Pooling: - For expensive-to-create objects - Balance with GC benefits of short-lived objects 4. Avoid Large Objects in Young Gen: - Large objects go directly to Old Gen - Wastes Young Gen space """); demonstrateObjectReuse(); } public static void demonstrateObjectReuse() { System.out.println("\n=== Object Reuse Example ==="); // Bad: Creating new objects repeatedly long startTime = System.nanoTime(); for (int i = 0; i < 10000; i++) { String message = new String("Message " + i); // Unnecessary new object } long newObjectTime = System.nanoTime() - startTime; // Good: Reusing objects startTime = System.nanoTime(); StringBuilder reusableBuilder = new StringBuilder(); for (int i = 0; i < 10000; i++) { reusableBuilder.setLength(0); reusableBuilder.append("Message ").append(i); String message = reusableBuilder.toString(); } long reuseTime = System.nanoTime() - startTime; System.out.printf("New object creation: %,d ns%n", newObjectTime); System.out.printf("Object reuse: %,d ns%n", reuseTime); System.out.printf("Improvement: %.2fx faster%n", (double) newObjectTime / reuseTime); } } Old Generation Allocation
public class OldGenAllocation { private static final List<byte[]> oldGenObjects = new ArrayList<>(); public static void main(String[] args) { demonstrateOldGenAllocation(); demonstratePromotionPatterns(); showOldGenFragmentation(); } public static void demonstrateOldGenAllocation() { System.out.println("=== Old Generation Allocation ==="); System.out.println(""" Old Gen Allocation Paths: 1. Promotion from Young Gen: - Objects surviving MaxTenuringThreshold GC cycles 2. Large Object Allocation: - Objects larger than TLAB size - Typically > 1MB (depends on JVM settings) 3. Explicit Old Gen Allocation: - Through bytecode manipulation (rare) """); // Demonstrate different allocation paths demonstratePromotionAllocation(); demonstrateLargeObjectAllocation(); } public static void demonstratePromotionAllocation() { System.out.println("\n=== Promotion to Old Generation ==="); // Create objects that will be promoted List<byte[]> promotionCandidates = new ArrayList<>(); for (int i = 0; i < 100; i++) { byte[] obj = new byte[2048]; // 2KB objects promotionCandidates.add(obj); oldGenObjects.add(obj); // Keep reference to ensure promotion if (i % 20 == 0) { System.out.printf("Creating promotion candidate %d%n", i); } } System.out.println("Created " + promotionCandidates.size() + " promotion candidates"); // Multiple GC hints to encourage promotion for (int gcCycle = 1; gcCycle <= 3; gcCycle++) { System.gc(); // Hint GC - for demonstration System.out.printf("GC cycle %d completed%n", gcCycle); try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } public static void demonstrateLargeObjectAllocation() { System.out.println("\n=== Large Object Allocation ==="); // Large objects typically go directly to Old Gen byte[] largeObject1 = new byte[2 * 1024 * 1024]; // 2MB byte[] largeObject2 = new byte[5 * 1024 * 1024]; // 5MB byte[] largeObject3 = new byte[10 * 1024 * 1024]; // 10MB oldGenObjects.add(largeObject1); oldGenObjects.add(largeObject2); oldGenObjects.add(largeObject3); System.out.println("Allocated large objects directly to Old Gen:"); System.out.printf(" Object 1: %,d bytes%n", largeObject1.length); System.out.printf(" Object 2: %,d bytes%n", largeObject2.length); System.out.printf(" Object 3: %,d bytes%n", largeObject3.length); showLargeObjectThreshold(); } public static void showLargeObjectThreshold() { System.out.println("\n=== Large Object Threshold ==="); System.out.println(""" Threshold Determination: - G1 GC: 50% of region size (default region = 1MB, threshold = 512KB) - Parallel GC: No specific threshold, based on TLAB size - CMS: Similar to Parallel GC JVM Options: - -XX:G1HeapRegionSize=<size> - -XX:+UnlockExperimentalVMOptions -XX:G1EagerReclaimHumongousObjects """); } public static void demonstratePromotionPatterns() { System.out.println("\n=== Promotion Patterns ==="); System.out.println(""" Common Promotion Scenarios: Steady-State Promotion: - Consistent rate of long-lived objects - Healthy for GC performance Promotion Storms: - Sudden large number of promotions - Can cause Old Gen fragmentation Premature Promotion: - Objects promoted too early - Wastes Old Gen space - Caused by too small Young Gen """); demonstratePrematurePromotion(); } public static void demonstratePrematurePromotion() { System.out.println("\n=== Premature Promotion Example ==="); System.out.println(""" Premature Promotion Causes: 1. Too Small Young Generation: - Young Gen fills quickly - Objects get promoted without proper aging 2. High Allocation Rate: - Objects don't get time to die in Young Gen 3. Large Objects: - Force early promotion """); // Simulate scenario causing premature promotion List<byte[]> temporaryObjects = new ArrayList<>(); // Allocate many objects quickly for (int batch = 0; batch < 10; batch++) { for (int i = 0; i < 1000; i++) { byte[] obj = new byte[1024]; // 1KB objects temporaryObjects.add(obj); } // Clear most objects but keep some (simulating mixed lifespan) if (batch % 3 == 0) { // Keep this batch for promotion oldGenObjects.addAll(temporaryObjects); } else { temporaryObjects.clear(); } } System.out.println("Premature promotion scenario simulated"); } public static void showOldGenFragmentation() { System.out.println("\n=== Old Generation Fragmentation ==="); System.out.println(""" Fragmentation Causes: 1. Mixed Object Sizes: - Small and large objects interleaved 2. Promotion Patterns: - Irregular promotion rates 3. GC Algorithm: - Mark-Sweep-Compact vs Mark-Sweep Symptoms: - OutOfMemoryError despite available free memory - Long GC pause times - High Old Gen utilization """); demonstrateFragmentationImpact(); } public static void demonstrateFragmentationImpact() { System.out.println("\n=== Fragmentation Impact ==="); // Allocate objects of different sizes allocateMixedSizeObjects(); System.out.println(""" Mitigation Strategies: 1. Use Compacting GC (G1, Parallel) 2. Avoid very large object allocations 3. Use object pools for specific sizes 4. Tune -XX:MaxTenuringThreshold 5. Increase heap size if possible """); } private static void allocateMixedSizeObjects() { Random random = new Random(); List<byte[]> fragmentedObjects = new ArrayList<>(); // Create fragmentation by allocating different sized objects for (int i = 0; i < 500; i++) { int size; if (random.nextDouble() < 0.7) { size = 1024; // 70% small objects } else if (random.nextDouble() < 0.9) { size = 64 * 1024; // 20% medium objects } else { size = 512 * 1024; // 10% large objects } byte[] obj = new byte[size]; fragmentedObjects.add(obj); oldGenObjects.add(obj); } System.out.printf("Allocated %,d mixed-size objects%n", fragmentedObjects.size()); } } 5. Advanced Allocation Techniques
Object Pooling Strategies
public class ObjectPoolingStrategies { public static void main(String[] args) { demonstrateObjectPooling(); comparePoolingStrategies(); showPoolingBestPractices(); } public static void demonstrateObjectPooling() { System.out.println("=== Object Pooling Strategies ==="); System.out.println(""" When to Use Object Pooling: ✅ Good Candidates: - Expensive-to-create objects (DB connections, threads) - Objects with heavy initialization - When allocation rate is very high - Real-time systems with strict latency requirements ❌ Poor Candidates: - Simple, small objects - Short-lived objects - When GC pressure is low """); // Demonstrate different pooling approaches demonstrateSimplePool(); demonstrateThreadLocalPool(); } public static class SimpleObjectPool<T> { private final Queue<T> pool; private final Supplier<T> creator; private final int maxSize; public SimpleObjectPool(int maxSize, Supplier<T> creator) { this.pool = new LinkedList<>(); this.creator = creator; this.maxSize = maxSize; } public T borrowObject() { T obj = pool.poll(); if (obj == null) { obj = creator.get(); } return obj; } public void returnObject(T obj) { if (pool.size() < maxSize) { pool.offer(obj); } // Else let GC handle it } public int getPoolSize() { return pool.size(); } } public static void demonstrateSimplePool() { System.out.println("\n=== Simple Object Pool ==="); SimpleObjectPool<StringBuilder> pool = new SimpleObjectPool<>(10, StringBuilder::new); // Use pooled objects List<String> results = new ArrayList<>(); for (int i = 0; i < 100; i++) { StringBuilder sb = pool.borrowObject(); try { sb.setLength(0); // Reset for reuse sb.append("Result ").append(i); results.add(sb.toString()); } finally { pool.returnObject(sb); } } System.out.printf("Pool size: %d, Results: %d%n", pool.getPoolSize(), results.size()); } public static class ThreadLocalObjectPool<T> { private final ThreadLocal<T> threadLocal; private final Supplier<T> creator; private final Consumer<T> resetter; public ThreadLocalObjectPool(Supplier<T> creator, Consumer<T> resetter) { this.creator = creator; this.resetter = resetter; this.threadLocal = ThreadLocal.withInitial(creator); } public T getObject() { T obj = threadLocal.get(); resetter.accept(obj); return obj; } } public static void demonstrateThreadLocalPool() { System.out.println("\n=== Thread-Local Object Pool ==="); ThreadLocalObjectPool<StringBuilder> threadLocalPool = new ThreadLocalObjectPool<>( StringBuilder::new, sb -> sb.setLength(0) ); // Each thread gets its own pooled object int threadCount = 5; ExecutorService executor = Executors.newFixedThreadPool(threadCount); for (int i = 0; i < threadCount; i++) { final int threadId = i; executor.execute(() -> { StringBuilder sb = threadLocalPool.getObject(); for (int j = 0; j < 10; j++) { sb.append("Thread-").append(threadId).append("-").append(j); String result = sb.toString(); sb.setLength(0); // Reset for next use } System.out.printf("Thread %d completed%n", threadId); }); } executor.shutdown(); try { executor.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static void comparePoolingStrategies() { System.out.println("\n=== Pooling Strategy Comparison ==="); int iterations = 10000; // Without pooling long startTime = System.nanoTime(); long withoutPoolingResult = testWithoutPooling(iterations); long withoutPoolingTime = System.nanoTime() - startTime; // With pooling startTime = System.nanoTime(); long withPoolingResult = testWithPooling(iterations); long withPoolingTime = System.nanoTime() - startTime; System.out.printf("Without pooling: %,d ns%n", withoutPoolingTime); System.out.printf("With pooling: %,d ns%n", withPoolingTime); System.out.printf("Pooling benefit: %.2fx%n", (double) withoutPoolingTime / withPoolingTime); System.out.println("Result verification: " + (withoutPoolingResult == withPoolingResult)); } private static long testWithoutPooling(int iterations) { long sum = 0; for (int i = 0; i < iterations; i++) { StringBuilder sb = new StringBuilder(); sb.append("Number ").append(i); sum += sb.toString().length(); } return sum; } private static final ThreadLocal<StringBuilder> threadLocalBuilder = ThreadLocal.withInitial(StringBuilder::new); private static long testWithPooling(int iterations) { long sum = 0; StringBuilder sb = threadLocalBuilder.get(); for (int i = 0; i < iterations; i++) { sb.setLength(0); sb.append("Number ").append(i); sum += sb.toString().length(); } return sum; } public static void showPoolingBestPractices() { System.out.println("\n=== Object Pooling Best Practices ==="); System.out.println(""" Best Practices: 1. Profile First: - Only pool if allocation is actually a bottleneck 2. Choose Right Pool Size: - Too small: frequent allocation anyway - Too large: memory waste 3. Handle Object Reset: - Properly reset object state before reuse 4. Consider Thread Safety: - Use ThreadLocal for thread-safe pools 5. Monitor Pool Usage: - Track hit rates and allocation patterns 6. Avoid Memory Leaks: - Clear pools when no longer needed """); } } 6. Monitoring and Tuning
Allocation Monitoring Tools
public class AllocationMonitoring { public static void main(String[] args) throws Exception { demonstrateAllocationMonitoring(); showJVMAllocationFlags(); demonstrateAllocationProfiling(); } public static void demonstrateAllocationMonitoring() { System.out.println("=== Allocation Monitoring ==="); System.out.println(""" Monitoring Tools: JVM Built-in: - -XX:+PrintGCDetails (shows allocation rates) - -XX:+PrintTLAB (TLAB allocation statistics) - -XX:+PrintPromotionFailure (promotion issues) External Tools: - JVisualVM (Allocation profiling) - Java Mission Control (Detailed allocation analysis) - YourKit (Allocation tracking) - JProfiler (Memory allocation views) """); demonstrateJMXAllocationMonitoring(); } public static void demonstrateJMXAllocationMonitoring() { System.out.println("\n=== JMX Allocation Monitoring ==="); try { // Get memory MXBean java.lang.management.MemoryMXBean memoryMXBean = java.lang.management.ManagementFactory.getMemoryMXBean(); // Get heap memory usage java.lang.management.MemoryUsage heapUsage = memoryMXBean.getHeapMemoryUsage(); System.out.printf("Heap Usage:%n"); System.out.printf(" Init: %,d bytes%n", heapUsage.getInit()); System.out.printf(" Used: %,d bytes%n", heapUsage.getUsed()); System.out.printf(" Committed: %,d bytes%n", heapUsage.getCommitted()); System.out.printf(" Max: %,d bytes%n", heapUsage.getMax()); // Get memory pool MXBeans List<java.lang.management.MemoryPoolMXBean> pools = java.lang.management.ManagementFactory.getMemoryPoolMXBeans(); for (java.lang.management.MemoryPoolMXBean pool : pools) { System.out.printf("%nMemory Pool: %s%n", pool.getName()); java.lang.management.MemoryUsage usage = pool.getUsage(); System.out.printf(" Used: %,d bytes%n", usage.getUsed()); System.out.printf(" Peak: %,d bytes%n", pool.getPeakUsage().getUsed()); } } catch (Exception e) { System.err.println("JMX monitoring failed: " + e.getMessage()); } } public static void showJVMAllocationFlags() { System.out.println("\n=== JVM Allocation Tuning Flags ==="); System.out.println(""" Key Allocation Flags: TLAB Settings: - -XX:TLABSize=<size> (initial TLAB size) - -XX:+ResizeTLAB (enable TLAB resizing - default: true) - -XX:MinTLABSize=<size> (minimum TLAB size) Young Generation: - -XX:NewSize=<size> (initial Young Gen size) - -XX:MaxNewSize=<size> (maximum Young Gen size) - -XX:NewRatio=<ratio> (Old Gen / Young Gen ratio) Promotion: - -XX:MaxTenuringThreshold=<age> (max age before promotion) - -XX:+NeverTenure / -XX:+AlwaysTenure (experimental) Large Objects: - -XX:PretenureSizeThreshold=<size> (size for direct Old Gen allocation) """); } public static void demonstrateAllocationProfiling() { System.out.println("\n=== Allocation Profiling ==="); System.out.println(""" Allocation Profiling Techniques: 1. Allocation Stack Traces: - -XX:+HeapDumpOnOutOfMemoryError - -XX:HeapDumpPath=<path> 2. Continuous Allocation Profiling: - Java Flight Recorder (JFR) - -XX:+FlightRecorder - -XX:StartFlightRecording=settings=profile 3. Object Allocation Tracking: - -XX:+AllocationProfiling - -XX:AllocationProfilingSize=<size> """); // Demonstrate simple allocation profiling simpleAllocationProfile(); } public static void simpleAllocationProfile() { System.out.println("\n=== Simple Allocation Profile ==="); long startTime = System.currentTimeMillis(); List<Object> allocatedObjects = new ArrayList<>(); Map<String, Integer> allocationByType = new HashMap<>(); // Profile allocation for a period for (int i = 0; i < 10000; i++) { Object obj; if (i % 4 == 0) { obj = new String("String_" + i); allocationByType.merge("String", 1, Integer::sum); } else if (i % 4 == 1) { obj = new Integer(i); allocationByType.merge("Integer", 1, Integer::sum); } else if (i % 4 == 2) { obj = new ArrayList<>(10); allocationByType.merge("ArrayList", 1, Integer::sum); } else { obj = new byte[1024]; allocationByType.merge("byte[]", 1, Integer::sum); } allocatedObjects.add(obj); } long endTime = System.currentTimeMillis(); System.out.println("Allocation Profile Results:"); System.out.printf("Total objects: %,d%n", allocatedObjects.size()); System.out.printf("Total time: %,d ms%n", endTime - startTime); allocationByType.forEach((type, count) -> { double percentage = (double) count / allocatedObjects.size() * 100; System.out.printf(" %s: %,d (%.1f%%)%n", type, count, percentage); }); } } Conclusion
Heap Allocation Strategy Summary:
| Strategy | Best For | Considerations |
|---|---|---|
| TLAB Allocation | Small objects, multi-threaded apps | Default, very efficient |
| Eden Allocation | Short-lived objects | Fast, cleared by Young GC |
| Large Object Allocation | Objects > TLAB size | Goes directly to Old Gen |
| Stack Allocation | No-escape objects | JVM optimization, not controllable |
| Object Pooling | Expensive objects, high allocation | Manual management required |
Performance Guidelines:
- Prefer short-lived objects in Young Generation when possible
- Use appropriate object sizes - avoid unnecessarily large objects
- Leverage TLAB benefits - write allocation-friendly code
- Monitor allocation rates and GC behavior
- Consider object reuse for high-allocation scenarios
- Profile before optimizing - don't guess about allocation bottlenecks
Tuning Recommendations:
- Increase Young Generation size if you have many short-lived objects
- Monitor promotion rates to detect premature promotion
- Use appropriate GC algorithm for your allocation patterns
- Consider object pooling only when allocation is proven bottleneck
- Enable allocation profiling in development to understand patterns
Understanding heap allocation strategies enables you to write memory-efficient Java applications and make informed decisions about JVM tuning and optimization.