JVM Concurrency and Memory Management Advanced Guide
Java Memory Model
Section titled “Java Memory Model”The JMM defines visibility and ordering guarantees across threads.
synchronized
Section titled “synchronized”- Mutual exclusion
- Visibility
- Happens-before relationship
volatile
Section titled “volatile”- Visibility
- No locking
- No atomicity
Garbage Collection
Section titled “Garbage Collection”Heap generations:
- Young Generation — short-lived objects, collected frequently (minor GC)
- Eden Space — where new objects are allocated
- Survivor Spaces — hold objects that survived at least one GC cycle
- Old (Tenured) Generation — long-lived objects, collected less often but more expensively (major GC)
- Metaspace — class metadata (replaced PermGen in Java 8)
Frequent minor GCs (Young Gen) are fast; major GCs (Old Gen) are slower — this is why tuning generation sizes for your allocation pattern matters more than just increasing total heap size.
Algorithms:
- Serial GC — single-threaded, simplest, fine for small apps
- Parallel GC (Throughput GC) — multiple threads for minor GC, maximizes throughput
- CMS (Concurrent Mark-Sweep) — low-pause GC for old generation, does most work concurrently with the app (deprecated/removed in newer JDKs)
- G1 GC (Garbage First) — modern default; divides the heap into regions and does concurrent marking with incremental compaction to minimize pause times
GC phases:
flowchart LR A[Mark] --> B[Sweep] B --> C[Compact]
ThreadLocal
Section titled “ThreadLocal”Each thread gets its own value.
Internally:
Thread |ThreadLocalMap |ThreadLocal -> Valueprivate static final ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> 0);
Runnable task = () -> { threadId.set((int) (Math.random() * 100)); System.out.println(Thread.currentThread().getName() + " : " + threadId.get());};
new Thread(task).start();new Thread(task).start();Each thread prints its own value — they never see each other’s, and no synchronization is needed since there’s nothing shared.
When to Use
Section titled “When to Use”- Per-thread state without synchronization: transaction context (e.g. Spring’s
TransactionSynchronizationManager), request-scoped data, user/auth context per request thread.
When Not to Use
Section titled “When Not to Use”- When you actually need to share data between threads —
ThreadLocaldoes the opposite by design. - Carelessly in thread pools: pooled threads are reused across tasks/requests, so a value set by one request can leak into the next unless cleared.
Always call this when using thread pools, to avoid leaking state between reused threads:
threadLocal.remove();Concurrency vs Parallelism
Section titled “Concurrency vs Parallelism”| Concurrency | Parallelism |
|---|---|
| Multiple tasks managed | Multiple tasks executed simultaneously |
Safe concurrency tools: ExecutorService, ConcurrentHashMap, AtomicInteger, ReentrantLock.
wait vs sleep vs yield
Section titled “wait vs sleep vs yield”wait()
Section titled “wait()”Defined on Object. Releases the monitor lock and puts the current thread into the waiting state until another thread calls notify()/notifyAll() on the same object. Used for inter-thread communication, typically inside a synchronized block.
synchronized (obj) { obj.wait(); // releases the lock and waits}sleep()
Section titled “sleep()”Defined on Thread. Pauses the current thread for a given time but does not release any lock it holds. Used for delaying execution, not synchronization.
Thread.sleep(1000); // pauses for 1 secondyield()
Section titled “yield()”Defined on Thread. A hint to the scheduler that the current thread is willing to give up its CPU turn so other threads of equal priority get a chance to run. It doesn’t release any lock, doesn’t return a value, and doesn’t guarantee anything — the scheduler is free to ignore it entirely.
Thread.yield();Comparison
Section titled “Comparison”| Method | Belongs To | Releases Lock | Purpose | Typical Use Case |
|---|---|---|---|---|
wait() |
Object |
Yes | Inter-thread communication | Producer-consumer pattern |
sleep() |
Thread |
No | Pause for specific time | Retry/backoff logic |
yield() |
Thread |
No | Give CPU to other threads | Scheduler hint only — rarely relied on in production |
Reference Types
Section titled “Reference Types”| Type | GC Behavior | Use |
|---|---|---|
| Strong | Never collected | Normal objects |
| Soft | Collected under memory pressure | Cache |
| Weak | Immediately eligible | WeakHashMap |
| Phantom | Cleanup tracking | Native resources |
Class Loaders
Section titled “Class Loaders”flowchart TD Bootstrap --> Platform Platform --> Application Application --> Custom
- Bootstrap ClassLoader loads core JDK classes (
java.lang.*,java.util.*, etc.) from the JDK itself. Implemented in native code, not Java. - Platform ClassLoader (called the Extension ClassLoader before Java 9) loads platform/extension libraries (historically from the
extdirectory). - Application ClassLoader loads your application’s classes from the classpath (
-cp/CLASSPATH) — the default loader for your own code. - Custom ClassLoader — developers can extend
ClassLoaderto load classes from non-standard locations or apply custom loading logic.
Parent Delegation Model
Section titled “Parent Delegation Model”Every class loader has a parent. When a class needs to be loaded, the request is delegated to the parent first; the current loader only attempts to load it itself if the parent can’t find it. This ensures core classes are always loaded by the Bootstrap loader — your application can’t shadow or override java.lang.String, for instance.
Real-World Uses for Custom ClassLoaders
Section titled “Real-World Uses for Custom ClassLoaders”- Plugin architectures — load plugins dynamically at runtime without restarting the app (e.g. how IDEs like Eclipse/IntelliJ load plugins).
- Hot deployment / hot swapping — reload classes in memory without a full JVM restart.
- Security / sandboxing — restrict what classes loaded code can access.
- Loading from unusual sources — network locations, databases, or encrypted JARs.
ForkJoinPool
Section titled “ForkJoinPool”Uses work stealing: idle worker threads pull tasks from busy workers’ queues instead of sitting idle.
flowchart LR A[Worker 1 Queue] B[Worker 2 Queue] B -->|Steals Task| A
Parallel Streams internally use ForkJoinPool (specifically the common pool, by default).
Key Takeaways
Section titled “Key Takeaways”synchronizedgives you mutual exclusion, visibility, and a happens-before relationship;volatilegives you only visibility and ordering, not exclusion.- G1 GC is the modern default for most workloads; CMS is deprecated. All GC algorithms fundamentally mark reachable objects, sweep the rest, and (for compacting collectors) defragment the heap.
- Always call
threadLocal.remove()when using thread pools — otherwise the value leaks into the next task that reuses the same pooled thread. - Reference type strength (Strong > Soft > Weak > Phantom) determines how aggressively the GC reclaims an object;
WeakHashMapuses Weak references so entries disappear once nothing else references the key. ForkJoinPool’s work-stealing lets idle threads pick up work from busy ones, which is why parallel streams scale well on uneven workloads without additional configuration.