Skip to content

JVM Concurrency and Memory Management Advanced Guide

The JMM defines visibility and ordering guarantees across threads.

  • Mutual exclusion
  • Visibility
  • Happens-before relationship
  • Visibility
  • No locking
  • No atomicity

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]

Each thread gets its own value.

Internally:

Thread
|
ThreadLocalMap
|
ThreadLocal -> Value
private 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.

  • Per-thread state without synchronization: transaction context (e.g. Spring’s TransactionSynchronizationManager), request-scoped data, user/auth context per request thread.
  • When you actually need to share data between threads — ThreadLocal does 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 Parallelism
Multiple tasks managed Multiple tasks executed simultaneously

Safe concurrency tools: ExecutorService, ConcurrentHashMap, AtomicInteger, ReentrantLock.

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
}

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 second

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();
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
Type GC Behavior Use
Strong Never collected Normal objects
Soft Collected under memory pressure Cache
Weak Immediately eligible WeakHashMap
Phantom Cleanup tracking Native resources
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 ext directory).
  • Application ClassLoader loads your application’s classes from the classpath (-cp/CLASSPATH) — the default loader for your own code.
  • Custom ClassLoader — developers can extend ClassLoader to load classes from non-standard locations or apply custom loading logic.

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.

  • 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.

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).

  • synchronized gives you mutual exclusion, visibility, and a happens-before relationship; volatile gives 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; WeakHashMap uses 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.