Handling multiple jobs in Java always caused trouble. For ages, developers managed thread pools by hand, tweaked stack sizes, saw garbage collection slow things down more and more as demand climbed. Now comes Java 21, with Project Loom, introducing lightweight virtual threads- making high-load applications simpler to shape. Beyond faster performance, a quieter question grows: what happens to Project Loom memory usage? In this article Through live trials, GC logs studied closely, answers drawn only from measured data.

What Are Virtual Threads in Project Loom? (Java 21 Overview)

A fresh kind of thread appears in Java 21, born from Project Loom – these are virtual threads, handled directly by the JVM. Instead of tying each one to an OS thread like regular platform threads do, they ride on a compact set of carrier threads. Because the JVM takes care of when they run, it becomes possible to launch vast numbers, sometimes millions, without burdening the operating system. Heavy costs tied to scaling with classic threads fade here. What really sets them apart lives inside how heap memory gets used. Starting with nearly zero small heap, virtual threads grow only when required, unlike platform threads reserving about 1 MB right away. Under a workload of 50,000 simultaneous operations, Project Loom memory usage shifts sharply- so does how garbage collection operates.

Fig: Platform threads (left) vs Virtual threads (right) – heavy and stressed vs lightweight and calm

Project Loom Memory Usage Explained: What Changes with Virtual Threads?

Understanding Project Loom memory usage requires looking at two things: 

1. How Virtual Threads Reduce Stack Memory Usage in Java

A single thread on any platform grabs just enough memory to function. Fifty thousand tasks managed by ten actual threads mean each lingers longer than ideal- this drags pressure onto garbage collection. Lightweight alternatives behave differently- they carry almost nothing, their details quietly managed inside Java’s core machinery. When one pauses for disk access or a signal across wires, it slips away cleanly, storing only what matters in a narrow block of RAM- all while freeing up space the OS can actually reuse.

2. How Virtual Threads Reduce Heap Pressure and GC Overhead

With a fixed thread pool, tasks queue up and objects accumulate in the Young generation in generational garbage collectors until garbage collection is triggered. This buildup increases GC activity and pause times. Virtual threads distribute work more evenly. Tasks complete sooner, and short-lived objects are released quickly. As a result, garbage collection runs less frequently, pause times shrink, and overall system interruption is reduced. 

How Project Loom Affects G1 Garbage Collection Performance

Since Java 9, G1GC has been the go-to garbage collector for apps needing speed without heavy delays. Splitting the heap into chunks helps it manage memory more smoothly than older methods. Instead of freezing everything at once, it marks unused objects while the app runs, thanks to the concurrent marking phase in garbage collection that shares time with active tasks. When the heap fills up, this marking phase kicks in automatically, and that extra monitoring demands processor time, causing brief, unexpected pauses.

Virtual threads change this picture entirely. Because they hold almost nothing in memory, heap pressure stays low and G1 rarely needs to start a background cycle. Our test confirmed this, not one concurrent cleanup happened with virtual threads running.

Benchmarking Project Loom Memory Usage: G1 GC Log Analysis

One way to see the impact of different threading models on G1GC is by running both tests under the same setup with Java 21. Fifty thousand tasks moved through sequentially, every single one reserving ten kilobytes from the heap. From start to finish, garbage collection behavior was recorded using the following JVM option.

-Xlog:gc*:file=gc.log

1. Memory Behavior with Platform Threads (Baseline Test)

A fixed pool of 10 threads handling 50,000 tasks, each allocating 10KB, enough to stress the heap and force repeated GC cycles. 

Here’s a sample snippet: 

ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 50_000; i++) {
    executor.submit(() -> {
        byte[] data = new byte[1024 * 10]; // 10KB
    });
}
executor.shutdown();

With just 10 threads working sequentially, used memory builds up faster than it gets cleared. Because of this pressure, G1 GC halts execution repeatedly to catch up, while its concurrent marking phase continues consuming CPU in the background. In total, the system freezes 14 times, alongside nearly a third of a second spent cleaning behind the scenes.

Here are some key highlights: 

  • 14 pauses tied to garbage collection happened while the test ran through completely
  • Lasted 325 milliseconds while handling background garbage collection at the same time. The marking step stayed on the entire period
  • Frozen moments pop up again and again – each freeze sharp enough to slow down real user actions. Pauses like these bite during live use, never gentle, always noticeable
  • Over time, the system’s memory fills up steadily because tasks line up one after another on a rigid set of threads

Fig: G1 GC Time, Platform Threads (Fixed Pool of 10, 50k tasks)

What to observe: Notice the large concurrent GC segment in the pie chart and high average pause times in the bar chart, G1 never got a break.

Result: Platform threads caused sustained GC pressure with frequent pauses.

2. Memory Behavior with Virtual Threads (Project Loom Test)

One virtual thread per task, 50,000 threads, each allocating 10KB, the JVM manages scheduling with near-zero stack overhead per thread.

Here’s a sample snippet: 

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 50_000; i++) {
    executor.submit(() -> {
        byte[] data = new byte[1024 * 10]; // 10KB
    });
}
executor.shutdown();

When virtual threads block, they pause – slipping into rest without hogging memory. Instead of piling up, tasks finish quickly while brief objects vanish right after use. Memory stays so light that the heap avoids hitting any pressure point. Background cleanup barely kicks in; G1 steps in just 2 times across the whole execution, yet its sweeping scan doesn’t begin once.

Here are some key highlights:

  • 2 GC pauses happened during the full test of 50,000 tasks. That span covered every operation without extra stops
  • Every now and then, a quiet pause in processing meant zero overlapping garbage collection. Background work simply stayed idle – no mark phase ever kicked in
  • Most of the time, there was barely any lag – performance held steady without hiccups. Right away, delays felt almost nonexistent. Through each task, smoothness stuck around. Even under load, interruptions stayed rare. From start to finish, flow remained unbroken
  • Still, heap pressure stayed minimal, leaving G1 with nearly no reason to respond

                                        Fig: G1 GC Time, Virtual Threads (Project Loom, 50k tasks)

What to observe: The near-empty pie chart and flat bar chart confirm it, G1 had almost nothing to do. This is what heap health looks like with virtual threads.

Result: Virtual threads kept GC activity minimal with near-zero heap pressure.

Into GCeasy went both GC logs, ready for analysis. The resulting G1 GC Time reports clearly highlighted the difference in GC behavior between the two approaches. Together, these results show how Project Loom memory usage reduces GC pressure compared to platform threads.

Project Loom vs Platform Threads: Memory Usage Comparison

Right here, you see everything laid out – GC performance numbers alongside the design choices driving them. Pulling straight from GCeasy outputs, these figures make sense once you know how each system is built. What shows up in tests ties back to how things are set up underneath.

FactorVirtual Threads (Project Loom)Platform Threads
Stack MemoryFew hundred bytes per thread, grows only as needed1 MB reserved per thread upfront, held for the full lifecycle
Heap PressureLow, tasks complete fast, objects freed quicklyHigh, objects queue up, heap fills before GC can act
GC FrequencyRare, 2 pause events in 50k-task benchmarkFrequent, 14 pause events under identical load
Concurrent GCZero, background marking phase never triggeredContinuous, G1 background work ran throughout the run
Pause TimeNegligible, steady, predictable latency325 ms of background cleanup, 4x longer total pauses
ScalabilityMillions of threads with near-zero OS overheadConstrained by OS thread limit 
Best ForI/O-heavy, high-concurrency, latency-sensitive microservicesCPU-bound tasks where thread count stays low

Each platform thread holds about 1 MB of stack space throughout its lifecycle. With only 10 threads handling 50,000 tasks, work gets queued, keeping memory consistently occupied. This sustained pressure forces G1 GC to run repeatedly, leading to frequent pauses and continuous background marking activity.

In contrast, virtual threads start with minimal memory and grow only when needed. When waiting for I/O or locks, they pause without holding resources. Tasks complete faster, short-lived objects are cleared quickly, and memory pressure remains low enough to avoid triggering GC cycles.

In real-world scenarios, such as waiting on databases or external services, platform threads remain idle while still consuming memory. As these delays accumulate, heap pressure increases and GC activity intensifies. Virtual threads allow systems to stay responsive under high concurrency, reducing latency spikes and improving consistency.

For CPU-bound workloads with minimal I/O waiting, the impact is less pronounced. Since threads remain actively executing, traditional thread models perform similarly, and virtual threads offer fewer advantages. Overall, for modern, high-concurrency applications, virtual threads provide a clear advantage in memory efficiency and GC behavior.

Key Insights: How Project Loom Improves Memory Efficiency and GC Behavior

Right away, the silence of the virtual thread run catches attention. Not far into the platform thread GC log, there’s clear sign of ongoing garbage collection paired with pauses- Behind the scenes, G1 was busy juggling memory needs. Yet switch to the view of virtual threads, and the constant activity fades out. Not a hint of quiet cleanup remains. Left with plenty of room in the heap, G1 stayed inactive, never called into motion. This is where Project Loom’s memory usage truly shows: not in cutting delays, yet in reshaping what the garbage collector even needs to do. The memory overhead of platform threads didn’t just accumulate objects over time, each one stirred frequent sweeps while running. Virtual threads handle info faster now, slipping through without stacking leftovers. Almost nothing piles up anymore, so the GC stays quiet most of the time. The workload didn’t drop- it simply lost its urgency. Speed alone isn’t the point. What shows up clearly is consistency. Fewer pauses for cleanup, no stepping on your own toes, barely any hiccups- this keeps things running true under pressure. When microservices stumble live, people feel it instantly. Smoothness through strain beats a single dazzling result every time.

MetricVirtual ThreadPlatform Thread
GC Pause EventsBarely interrupted, JVM paused only twice during the entire runForced into repeated stops throughout, GC kept intervening to keep up
Concurrent GCCompletely silent, G1 never needed to start background cleanup workContinuously active, draining CPU in the background the whole time
Total Pause TimeApplication stayed responsive and consistent under full loadFrozen 4x longer, users would feel this as latency spikes in production
PredictabilityConsistent, minor pauses only, no surprises, no spikesUnpredictable bursts with high variance, hard to tune, hard to trust

Conclusion: Why Project Loom Memory Usage Matters for Modern Java Applications

That day I began testing, one thing puzzled me. Switching to virtual threads in Java 21- how would it truly affect garbage collection? GCeasy spat out results sharper than I’d imagined. From start to finish, heap stress hardly built up- garbage collection stepped in only twice. Virtual threads meant fewer interruptions, zero background marking phases, a much lighter load overall. Meanwhile, platform threads pushed G1 into constant motion: fourteen pauses occurred, alongside 325 milliseconds of ongoing cleanup tasks behind the scenes. Here’s what changes when Project Loom memory usage reshapes how your JVM behaves. When your microservices juggle lots of async tasks while chasing steady response times, this behavior becomes far more relevant than peak throughput stats. Instead of fixating on how much it handles at once, pay attention to how smoothly it runs under pressure.

You can dig into the full GC analysis yourself over at GCeasy – upload your own logs and see how your app compares.

FAQs on Project Loom Memory Usage

It depends on scale. Each regular thread grabs about one megabyte just sitting there. Ten of them stay active the whole time, doing fifty thousand jobs. That much reserved space adds up fast. Virtual ones begin nearly empty. They take more only when they actually need it. Switching to virtual threads made background GC cycles vanish during our testing. Not once did the heap grow full enough to trigger them.

A mere handful of hundred bytes make up the starting size of Java 21’s virtual threads, far less than the megabyte usually tied to regular ones. During testing with fifty thousand tasks each reserving ten kilobytes, these lightweight threads barely nudged the heap, G1 garbage collection interrupted things just two times throughout. Regular threads? They brought on fourteen pauses, along with three hundred twenty five milliseconds spent cleaning up behind the scenes under the same workload.

Absolutely, in a big way, especially in I/O-heavy scenarios. With virtual threads using minimal memory between steps, they release the CPU whenever waiting, so temporary data vanishes right away. The G1 garbage collector hardly needs to step in. Our test showed no ongoing concurrent GC activity under virtual threads, the silent background scan that eats up processing power simply never began during the run. Regular platform threads, on the other hand, kept G1 constantly working, causing sudden delays in response times that real users would notice instantly.