Project Loom introduces virtual threads (lightweight threads) to Java, revolutionizing concurrency. However, this new paradigm requires updated monitoring approaches to effectively observe and debug virtual thread behavior.
Why Monitor Virtual Threads?
Virtual threads behave differently from platform threads:
- Massive concurrency - Thousands/millions of virtual threads
- Different scheduling - Managed by JVM, not OS
- Resource usage - Lower memory footprint
- Blocking behavior - Different I/O patterns
Built-in Monitoring Tools
1. JDK Flight Recorder (JFR) Events
Project Loom introduces new JFR events specifically for virtual threads:
import jdk.jfr.consumer.RecordingStream;
import java.time.Duration;
public class JFRVirtualThreadMonitoring {
public static void startVirtualThreadMonitoring() {
try (var rs = new RecordingStream()) {
// Enable virtual thread events
rs.enable("jdk.VirtualThreadStart");
rs.enable("jdk.VirtualThreadEnd");
rs.enable("jdk.VirtualThreadPinned");
rs.enable("jdk.VirtualThreadSubmitFailed");
rs.enable("jdk.VirtualThreadQueueFull");
// Consume events
rs.onEvent("jdk.VirtualThreadStart", event -> {
System.out.printf("VirtualThreadStart: thread=%s, carrier=%s%n",
event.getString("thread"),
event.getString("carrierThread"));
});
rs.onEvent("jdk.VirtualThreadPinned", event -> {
System.out.printf("VirtualThreadPinned: thread=%s, duration=%s%n",
event.getString("thread"),
event.getDuration("duration"));
});
rs.onEvent("jdk.VirtualThreadQueueFull", event -> {
System.out.printf("VirtualThreadQueueFull: count=%d%n",
event.getLong("activeThreadCount"));
});
rs.startAsync();
// Keep monitoring for 1 minute
Thread.sleep(60000);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
startVirtualThreadMonitoring();
}
}
2. Java Management Extensions (JMX) Monitoring
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import javax.management.MBeanServer;
import javax.management.ObjectName;
public class JMXVirtualThreadMonitor {
public static void monitorVirtualThreads() throws Exception {
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
// Check if virtual thread monitoring is supported
if (threadBean.isVirtualThreadSupported()) {
System.out.println("Virtual thread monitoring supported");
// Register custom MBean for virtual thread monitoring
VirtualThreadMetrics metrics = new VirtualThreadMetrics();
ObjectName name = new ObjectName("com.example:type=VirtualThreadMetrics");
mbs.registerMBean(metrics, name);
// Start monitoring loop
while (true) {
printVirtualThreadStats(threadBean, metrics);
Thread.sleep(5000);
}
}
}
private static void printVirtualThreadStats(ThreadMXBean threadBean,
VirtualThreadMetrics metrics) {
long virtualThreadCount = threadBean.getVirtualThreads().size();
long platformThreadCount = threadBean.getThreadCount() - virtualThreadCount;
System.out.printf("Virtual Threads: %d, Platform Threads: %d, Total: %d%n",
virtualThreadCount, platformThreadCount, threadBean.getThreadCount());
System.out.printf("Peak Virtual Threads: %d%n",
threadBean.getPeakVirtualThreadCount());
}
// MBean interface for virtual thread metrics
public interface VirtualThreadMetricsMBean {
long getVirtualThreadsCreated();
long getVirtualThreadsTerminated();
long getPinnedEventsCount();
}
// MBean implementation
public static class VirtualThreadMetrics implements VirtualThreadMetricsMBean {
private long vthreadsCreated = 0;
private long vthreadsTerminated = 0;
private long pinnedEvents = 0;
public void incrementCreated() { vthreadsCreated++; }
public void incrementTerminated() { vthreadsTerminated++; }
public void incrementPinnedEvents() { pinnedEvents++; }
@Override public long getVirtualThreadsCreated() { return vthreadsCreated; }
@Override public long getVirtualThreadsTerminated() { return vthreadsTerminated; }
@Override public long getPinnedEventsCount() { return pinnedEvents; }
}
}
Custom Monitoring Utilities
3. Virtual Thread Pool Monitor
import java.lang.reflect.Field;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.LockSupport;
public class VirtualThreadPoolMonitor {
private final AtomicLong totalVirtualThreads = new AtomicLong();
private final AtomicLong activeVirtualThreads = new AtomicLong();
private final AtomicLong completedTasks = new AtomicLong();
private final AtomicLong pinnedEvents = new AtomicLong();
private final AtomicLong failedSubmissions = new AtomicLong();
private final ScheduledExecutorService monitorScheduler =
Executors.newSingleThreadScheduledExecutor();
public void startMonitoring(ExecutorService virtualThreadExecutor) {
// Start metrics collection
monitorScheduler.scheduleAtFixedRate(() -> {
collectMetrics(virtualThreadExecutor);
}, 0, 1, TimeUnit.SECONDS);
// Start JFR event listener
startJFREventListener();
}
private void collectMetrics(ExecutorService executor) {
try {
// Use reflection to access internal virtual thread pool metrics
// Note: This is implementation-specific and may change
Field schedulerField = executor.getClass().getDeclaredField("scheduler");
schedulerField.setAccessible(true);
Object scheduler = schedulerField.get(executor);
// Collect various metrics
printMetrics();
} catch (Exception e) {
System.err.println("Failed to collect metrics: " + e.getMessage());
}
}
private void printMetrics() {
System.out.printf(
"Virtual Thread Metrics - Total: %d, Active: %d, Completed: %d, " +
"Pinned: %d, Failed: %d%n",
totalVirtualThreads.get(),
activeVirtualThreads.get(),
completedTasks.get(),
pinnedEvents.get(),
failedSubmissions.get()
);
}
private void startJFREventListener() {
Thread.startVirtualThread(() -> {
try (var rs = new RecordingStream()) {
rs.enable("jdk.VirtualThreadStart").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.VirtualThreadEnd").withPeriod(Duration.ofSeconds(1));
rs.enable("jdk.VirtualThreadPinned").withPeriod(Duration.ofSeconds(1));
rs.onEvent("jdk.VirtualThreadStart", event -> {
totalVirtualThreads.incrementAndGet();
activeVirtualThreads.incrementAndGet();
});
rs.onEvent("jdk.VirtualThreadEnd", event -> {
activeVirtualThreads.decrementAndGet();
completedTasks.incrementAndGet();
});
rs.onEvent("jdk.VirtualThreadPinned", event -> {
pinnedEvents.incrementAndGet();
});
rs.onEvent("jdk.VirtualThreadSubmitFailed", event -> {
failedSubmissions.incrementAndGet();
});
rs.start();
} catch (Exception e) {
e.printStackTrace();
}
});
}
public void stopMonitoring() {
monitorScheduler.shutdown();
}
// Example usage
public static void main(String[] args) throws Exception {
ExecutorService vthreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
VirtualThreadPoolMonitor monitor = new VirtualThreadPoolMonitor();
monitor.startMonitoring(vthreadExecutor);
// Submit some work
for (int i = 0; i < 1000; i++) {
final int taskId = i;
vthreadExecutor.submit(() -> {
System.out.println("Task " + taskId + " running on: " + Thread.currentThread());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
Thread.sleep(10000);
monitor.stopMonitoring();
vthreadExecutor.shutdown();
}
}
4. Virtual Thread Dump Utility
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class VirtualThreadDump {
private final ThreadMXBean threadMXBean;
private final Map<Long, String> virtualThreadNames;
public VirtualThreadDump() {
this.threadMXBean = ManagementFactory.getThreadMXBean();
this.virtualThreadNames = new ConcurrentHashMap<>();
}
public void captureThreadDump() {
System.out.println("=== VIRTUAL THREAD DUMP ===");
System.out.println("Timestamp: " + java.time.Instant.now());
// Get all threads
ThreadInfo[] allThreads = threadMXBean.dumpAllThreads(true, true);
// Separate virtual and platform threads
Arrays.stream(allThreads)
.filter(this::isVirtualThread)
.forEach(this::printVirtualThreadInfo);
printSummary();
}
private boolean isVirtualThread(ThreadInfo threadInfo) {
// Virtual threads have specific characteristics
return threadInfo.getThreadName().contains("/") ||
threadInfo.getThreadName().startsWith("VirtualThread");
}
private void printVirtualThreadInfo(ThreadInfo threadInfo) {
System.out.printf("%n\"%s\" #%d%n",
threadInfo.getThreadName(), threadInfo.getThreadId());
System.out.printf(" State: %s%n", threadInfo.getThreadState());
if (threadInfo.getLockName() != null) {
System.out.printf(" Locked on: %s%n", threadInfo.getLockName());
}
if (threadInfo.getLockOwnerName() != null) {
System.out.printf(" Lock owner: %s (%d)%n",
threadInfo.getLockOwnerName(), threadInfo.getLockOwnerId());
}
// Stack trace
System.out.println(" Stack:");
Arrays.stream(threadInfo.getStackTrace())
.forEach(frame -> System.out.printf(" at %s%n", frame));
}
private void printSummary() {
long virtualThreadCount = Arrays.stream(threadMXBean.dumpAllThreads(true, true))
.filter(this::isVirtualThread)
.count();
long platformThreadCount = threadMXBean.getThreadCount() - virtualThreadCount;
System.out.printf("%n=== SUMMARY ===%n");
System.out.printf("Virtual Threads: %d%n", virtualThreadCount);
System.out.printf("Platform Threads: %d%n", platformThreadCount);
System.out.printf("Total Threads: %d%n", threadMXBean.getThreadCount());
System.out.printf("Peak Virtual Thread Count: %d%n",
threadMXBean.getPeakVirtualThreadCount());
}
// Monitor virtual thread creation and termination
public void trackVirtualThreadLifecycle() {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
Thread.startVirtualThread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread[] allThreads = new Thread[bean.getThreadCount()];
bean.getThreadInfo(bean.getAllThreadIds());
Arrays.stream(allThreads)
.filter(Thread::isVirtual)
.forEach(vthread -> {
virtualThreadNames.putIfAbsent(
vthread.threadId(), vthread.getName());
});
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
public static void main(String[] args) throws Exception {
VirtualThreadDump monitor = new VirtualThreadDump();
monitor.trackVirtualThreadLifecycle();
// Create some virtual threads
for (int i = 0; i < 10; i++) {
Thread.startVirtualThread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
Thread.sleep(1000);
monitor.captureThreadDump();
}
}
Advanced Monitoring with Micrometer
5. Micrometer Metrics for Virtual Threads
import io.micrometer.core.instrument.*;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class VirtualThreadMicrometerMonitor {
private final MeterRegistry meterRegistry;
private final ExecutorService virtualThreadExecutor;
private final AtomicLong virtualThreadsCreated;
private final AtomicLong virtualThreadsTerminated;
private final AtomicLong pinnedEvents;
public VirtualThreadMicrometerMonitor(MeterRegistry registry) {
this.meterRegistry = registry;
this.virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
this.virtualThreadsCreated = new AtomicLong();
this.virtualThreadsTerminated = new AtomicLong();
this.pinnedEvents = new AtomicLong();
setupMetrics();
setupJFRIntegration();
}
private void setupMetrics() {
// Custom metrics for virtual threads
Gauge.builder("virtual.threads.active")
.description("Number of active virtual threads")
.register(meterRegistry, this, monitor ->
ManagementFactory.getThreadMXBean().getVirtualThreads().size());
Gauge.builder("virtual.threads.created.total")
.description("Total virtual threads created")
.register(meterRegistry, virtualThreadsCreated, AtomicLong::get);
Gauge.builder("virtual.threads.terminated.total")
.description("Total virtual threads terminated")
.register(meterRegistry, virtualThreadsTerminated, AtomicLong::get);
Gauge.builder("virtual.threads.pinned.events")
.description("Virtual thread pinned events")
.register(meterRegistry, pinnedEvents, AtomicLong::get);
// Platform thread metrics for comparison
Gauge.builder("platform.threads.active")
.description("Number of active platform threads")
.register(meterRegistry, this, monitor -> {
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
return bean.getThreadCount() - bean.getVirtualThreads().size();
});
// Monitor executor service
ExecutorServiceMetrics.monitor(meterRegistry, virtualThreadExecutor, "virtual.thread.executor");
}
private void setupJFRIntegration() {
Thread.startVirtualThread(() -> {
try (var rs = new RecordingStream()) {
rs.enable("jdk.VirtualThreadStart");
rs.enable("jdk.VirtualThreadEnd");
rs.enable("jdk.VirtualThreadPinned");
rs.onEvent("jdk.VirtualThreadStart", event -> {
virtualThreadsCreated.incrementAndGet();
Counter.builder("virtual.threads.started")
.register(meterRegistry)
.increment();
});
rs.onEvent("jdk.VirtualThreadEnd", event -> {
virtualThreadsTerminated.incrementAndGet();
});
rs.onEvent("jdk.VirtualThreadPinned", event -> {
pinnedEvents.incrementAndGet();
Counter.builder("virtual.threads.pinned")
.register(meterRegistry)
.increment();
});
rs.start();
} catch (Exception e) {
e.printStackTrace();
}
});
}
public ExecutorService getExecutor() {
return virtualThreadExecutor;
}
public void shutdown() {
virtualThreadExecutor.shutdown();
try {
if (!virtualThreadExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
virtualThreadExecutor.shutdownNow();
}
} catch (InterruptedException e) {
virtualThreadExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
Performance Testing Utility
6. Virtual Thread Performance Benchmark
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.stream.IntStream;
public class VirtualThreadBenchmark {
public static class BenchmarkResult {
public final long totalTime;
public final long tasksCompleted;
public final long virtualThreadsCreated;
public final long pinnedEvents;
public final double throughput;
public BenchmarkResult(long totalTime, long tasksCompleted,
long virtualThreadsCreated, long pinnedEvents) {
this.totalTime = totalTime;
this.tasksCompleted = tasksCompleted;
this.virtualThreadsCreated = virtualThreadsCreated;
this.pinnedEvents = pinnedEvents;
this.throughput = (double) tasksCompleted / totalTime * 1000;
}
}
public static BenchmarkResult runBenchmark(int taskCount,
int taskDurationMs,
ExecutorService executor) {
AtomicLong completedTasks = new AtomicLong();
AtomicLong pinnedEvents = new AtomicLong();
CountDownLatch latch = new CountDownLatch(taskCount);
long startTime = System.currentTimeMillis();
// Setup JFR monitoring for pinned events
Thread monitorThread = Thread.startVirtualThread(() -> {
try (var rs = new RecordingStream()) {
rs.enable("jdk.VirtualThreadPinned");
rs.onEvent("jdk.VirtualThreadPinned", event -> {
pinnedEvents.incrementAndGet();
});
rs.start();
} catch (Exception e) {
e.printStackTrace();
}
});
// Submit tasks
IntStream.range(0, taskCount).forEach(i -> {
executor.submit(() -> {
try {
// Simulate work
Thread.sleep(taskDurationMs);
completedTasks.incrementAndGet();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown();
}
});
});
// Wait for completion
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
long endTime = System.currentTimeMillis();
monitorThread.interrupt();
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
long virtualThreadsCreated = threadBean.getPeakVirtualThreadCount();
return new BenchmarkResult(
endTime - startTime,
completedTasks.get(),
virtualThreadsCreated,
pinnedEvents.get()
);
}
public static void main(String[] args) {
System.out.println("=== Virtual Thread Benchmark ===");
// Test with virtual threads
try (ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor()) {
BenchmarkResult virtualResult = runBenchmark(10000, 10, virtualExecutor);
printResult("Virtual Threads", virtualResult);
}
// Test with platform threads for comparison
try (ExecutorService platformExecutor = Executors.newFixedThreadPool(200)) {
BenchmarkResult platformResult = runBenchmark(10000, 10, platformExecutor);
printResult("Platform Threads", platformResult);
}
}
private static void printResult(String name, BenchmarkResult result) {
System.out.printf("%n%s Results:%n", name);
System.out.printf(" Total Time: %d ms%n", result.totalTime);
System.out.printf(" Tasks Completed: %d%n", result.tasksCompleted);
System.out.printf(" Virtual Threads Created: %d%n", result.virtualThreadsCreated);
System.out.printf(" Pinned Events: %d%n", result.pinnedEvents);
System.out.printf(" Throughput: %.2f tasks/second%n", result.throughput);
}
}
Best Practices for Virtual Thread Monitoring
- Enable JFR: Always enable Flight Recorder for detailed virtual thread events
- Monitor Pinned Events: High pinned event counts indicate synchronization issues
- Track Carrier Threads: Monitor platform thread utilization
- Use Appropriate Tools: JDK Mission Control, JFR, and custom monitoring
- Set Alerts: Configure alerts for abnormal virtual thread behavior
- Profile Regularly: Use async profilers to identify bottlenecks
Key Metrics to Monitor
- Virtual thread creation rate
- Virtual thread termination rate
- Pinned event frequency
- Carrier thread utilization
- Memory usage patterns
- Task completion rates
- Queue sizes and backpressure
Conclusion
Effective monitoring of Project Loom's virtual threads requires:
Essential Tools:
- JFR for detailed event tracking
- JMX for runtime metrics
- Custom monitors for application-specific insights
- Micrometer for integration with observability platforms
Critical Metrics:
- Virtual thread lifecycle events
- Pinning behavior
- Carrier thread utilization
- Memory and performance characteristics
Best Practices:
- Monitor early and often
- Set up alerts for abnormal patterns
- Combine multiple monitoring approaches
- Profile under realistic load conditions
Proper monitoring ensures you can leverage virtual threads effectively while maintaining application stability and performance.
Next Steps: Integrate these monitoring approaches with your existing observability stack and establish baselines for normal virtual thread behavior in your specific application context.