Skip to content

Skip per-thread event pooling on virtual threads#17

Merged
merlimat merged 1 commit into
mainfrom
skip-pooling-on-virtual-threads
Jun 10, 2026
Merged

Skip per-thread event pooling on virtual threads#17
merlimat merged 1 commit into
mainfrom
skip-pooling-on-virtual-threads

Conversation

@merlimat

Copy link
Copy Markdown
Owner

Summary

Virtual threads are typically short-lived and can exist in the millions: the per-thread PooledEmitState would be allocated on a vthread's first log call, used a handful of times, and abandoned — strictly worse than allocating a fresh event per emit, and the pool's memory footprint would scale with the number of live virtual threads (a million vthreads × ~500 B of pooled state is real memory).

  • emit() now gates the pool claim on Thread.isVirtual(), checked before touching the ThreadLocal so the per-vthread entry is never created; virtual threads fall through to the existing fresh-allocation path (the same one the thread-locals-disabled mode uses).
  • Sources compile at Java 17, where isVirtual() doesn't exist: the check is a MethodHandle resolved once at class init — the real (intrinsified) method on Java 21+, or a constant-false handle on older JVMs that the JIT folds away entirely. invokeExact on a static final handle inlines like a direct call.
  • README documents the caveat next to the zero-allocation claim it qualifies.

Testing

  • New Log4j2VirtualThreadTest — virtual threads started reflectively (tests also compile at the 17 source level), the vthread tests gated with @EnabledForJreRange(min = JAVA_21) and executing for real on the build JDK: platform-thread detection, vthread detection, and an end-to-end logging smoke test from a virtual thread with context + event attrs intact.
  • Platform-thread cost measured with rigorous JMH settings (3 forks × 5+5 iterations, GC profiler): slogSimple_enabled 16.65 ± 0.48 → 16.60 ± 0.15 ops/μs — statistically indistinguishable, zero-allocation property preserved.
  • Full check passes (all test tasks).

Virtual threads are typically short-lived and can exist in the
millions: the per-thread PooledEmitState would be allocated on a
vthread's first log call, used a handful of times, and abandoned -
worse than allocating a fresh event per emit, and the pool's memory
footprint would scale with the number of live virtual threads.

emit() now gates the pool claim on Thread.isVirtual(), checked before
touching the ThreadLocal so the per-vthread entry is never created;
virtual threads fall through to the existing fresh-allocation path.
Sources compile at Java 17, where isVirtual() does not exist, so the
check is a MethodHandle resolved once at class initialization: the
real (intrinsified) method on Java 21+, or a constant-false handle on
older JVMs that the JIT folds away.

Add Log4j2VirtualThreadTest (vthreads started reflectively, gated on
JRE >= 21): platform-thread detection, vthread detection, and an
end-to-end logging smoke test from a virtual thread. Platform-thread
cost measured with 3-fork JMH runs: 16.65±0.48 -> 16.60±0.15 ops/us,
statistically indistinguishable, zero allocation preserved. README
documents the caveat next to the zero-allocation claim.
@merlimat merlimat merged commit 1d9f88e into main Jun 10, 2026
7 checks passed
@merlimat merlimat deleted the skip-pooling-on-virtual-threads branch June 10, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant