Skip to content
Back
JVM internals

Virtual Threads and JVM Memory

Why virtual thread stacks live on the heap, what that does to allocation and GC pressure, and how to tune memory for virtual-thread workloads.

Michał Artur Marciniak

2 min read

On this page
  1. Virtual Threads and Memory — A New Allocation Paradigm
  2. Further Reading

The first two parts treated the stack and the heap as separate worlds. Virtual threads blur that line: their stacks are stored on the heap, which changes how an application allocates memory and how hard the collector has to work.

Virtual Threads and Memory — A New Allocation Paradigm

The Million-Thread Challenge

Java 21 introduced Virtual Threads (JEP 444), fundamentally changing how Java applications handle concurrency. Where platform threads map 1:1 to operating system threads (typically limited to thousands), virtual threads are lightweight user-mode threads that the JVM can multiplex onto a small pool of platform threads — enabling millions of concurrent tasks.

But this concurrency revolution comes with a memory management trade-off: virtual thread stack frames (continuations) are heap-allocated when the thread is parked or suspended. When millions of virtual threads are blocked waiting for I/O, their stack state lives on the heap rather than in native memory. This creates a fundamentally different allocation pattern than traditional thread pools.

How Virtual Threads Change Memory Allocation

The Traditional Model:

  • Platform threads: 1MB+ stack per thread (off-heap, in native memory)
  • Limited to thousands of threads due to OS constraints
  • Heap allocation comes primarily from application objects

The Virtual Thread Model:

  • Virtual threads: ~1KB initial footprint when idle
  • When mounted: runs on carrier thread’s native stack (like platform threads)
  • When parked/suspended: continuation stack frames stored on the heap
  • Millions of parked threads = millions of heap-resident continuations
  • Result: Persistent old-generation pressure rather than transient young-gen spikes

Real-World Findings

Research from InfoQ’s case study (2024) reveals:

  1. Memory footprint varies dramatically based on workload patterns. Applications with high virtual thread churn may see increased GC pressure compared to equivalent platform thread implementations due to heap-resident continuations.

  2. CPU-intensive workloads may show lower throughput with virtual threads than well-tuned platform thread pools due to increased allocation overhead from managing millions of thread states.

  3. I/O-bound workloads benefit most — the blocking operations that make virtual threads shine don’t increase heap pressure proportionally since threads are parked during I/O waits, and the heap storage of continuations is more memory-efficient than native thread stacks.

Tuning for Virtual Threads

Shell
# Monitor virtual thread pinning (when a VT cannot be unmounted):
# JDK Flight Recorder records a jdk.VirtualThreadPinned event
java -XX:StartFlightRecording=filename=app.jfr ...
jfr print --events jdk.VirtualThreadPinned app.jfr

# Adjust for higher allocation rates from virtual thread stacks
java -XX:+UseZGC -Xms4g -Xmx4g MyApp

# Consider larger young generation for VT-heavy workloads with G1
java -XX:+UseG1GC -XX:NewRatio=1 -XX:MaxGCPauseMillis=50 ...

Key Insight: When migrating to virtual threads, re-benchmark your memory allocation and GC behavior. The allocation profile may shift enough to warrant reconsidering your collector choice or tuning parameters. The collector that worked well for your platform-threaded application may not be optimal for the same workload using virtual threads.

Reference: Chirumamilla, P. et al. “Java Virtual Threads: a Case Study.” InfoQ, July 2024. The study found that “memory footprint in Open Liberty deployments can vary greatly based on factors like application design, workload level, and garbage collection behavior, so the reduced footprint of virtual threads may not result in an overall reduction in memory used.”


Further Reading