- Prerequisites
- B2V4 1.1 Process vs Thread
- Covers
- OS threads · JVM threads · Costs · Limits
- Related
- 1.2 Virtual Threads · 1.4 Comparison · B2V3 4.3 (container sizing)
On this page
1Concept / Definition
A platform thread is a java.lang.Thread implemented as a thin wrapper around an OS thread: HotSpot creates one native thread per platform thread and lets the operating system schedule it. Besides application threads, every JVM runs its own JVM threads — GC, JIT compilers, the VM thread and others. Platform threads carry real costs (stack memory, creation, context switches, safepoint work) and hit hard limits (OS and container thread limits, native memory), which is why they are pooled and why their count bounds classic thread-per-request servers.

2Why It Exists
Mapping Java threads 1:1 onto OS threads gave Java true parallelism on multi-core machines and let the OS handle preemption, priorities and blocking system calls. It remains the right model for CPU-bound work and for code that must run on an OS thread (native interop, long computations). Its costs only become a problem when a program needs very many concurrent threads that mostly wait.
3Core Concepts
OS threads
- Created with
pthread_create(Linux) — a kernel-schedulable entity with its own id (visible as LWP/TID), register state and a native stack. - Scheduled preemptively in time slices across cores; blocked threads (I/O, locks, sleep) are removed from the run queue by the kernel.
- A blocked platform thread still occupies its OS thread and stack — waiting is not free.
JVM threads

- A small application routinely has 20–40 JVM-internal threads;
jcmd Thread.printshows them all. - GC and compiler thread counts derive from available CPUs — in containers, from the CPU limit.
- With virtual threads, the scheduler's carrier threads (
ForkJoinPool-1-worker-N) are platform threads too.
Costs

Limits

With thread-per-request, maximum concurrency equals maximum threads. If each request spends 90% of its time waiting for I/O, a 200-thread pool on 8 cores keeps cores mostly idle while requests queue — the core motivation for virtual threads (and earlier for async/reactive programming).
4How It Works Internally
Thread.start() on a platform thread allocates a JavaThread, reserves the stack (-Xss or builder stackSize) with guard pages, creates the native thread and registers it for safepoints and GC root scanning. Stack pages are committed on demand as frames are pushed. On termination the native thread exits and the stack is unmapped.
5APIs / Features / Syntax
Thread worker = Thread.ofPlatform()
.name("report-", 1)
.stackSize(512 * 1024) // hint; -Xss sets the default
.daemon(false)
.start(this::generateReports);
System.out.println(worker.isVirtual()); // false
# Observe platform threads
$ jcmd <pid> Thread.print | grep -c '^"' # all platform threads
$ ps -L -p <pid> | wc -l # OS threads
$ cat /sys/fs/cgroup/pids.max # container limit on tasks (threads)
$ ulimit -u6Practical Examples
| Symptom | Cause | Direction |
|---|---|---|
| OutOfMemoryError: unable to create native thread | thread limit or native memory | bound pools; virtual threads for I/O |
| request latency rises, CPU at 20% | all pool threads blocked on I/O | larger pool / virtual threads + downstream limits |
| long safepoint pauses with 5,000 threads | many stacks to scan | fewer threads |
| container RSS far above heap | thread stacks and structures | count threads; NMT 'Thread' |
7Common Problems & Mistakes
- Unbounded thread creation (new thread per task, cached pools).
- Sizing I/O pools like CPU pools.
- Forgetting container
pids.maxand memory limits. - Ignoring JVM-internal threads when budgeting CPU.
8Performance & Production Considerations
- Monitor live thread count, peak count and per-state counts.
- Use NMT to see stack memory (
Threadcategory). - Keep platform threads for CPU work, carriers and special cases; move blocking I/O concurrency to virtual threads.
9Architecture & Design Considerations
Platform threads are a scarce resource, and the number a service can run shapes its whole concurrency model. With a classic thread-per-request server, maximum concurrency equals the size of the request thread pool, for example 200 threads in Tomcat's default configuration. When requests spend most of their time waiting on databases or remote calls, the pool fills up long before the CPUs are busy, and new requests queue or are rejected. Historically, architects responded by moving to asynchronous or reactive designs, which serve many requests with few threads but make code harder to write, debug and profile.
Since Java 21, virtual threads change that trade-off for I/O-bound services: they keep the simple blocking programming model while removing the platform-thread limit. Platform threads remain the right building block for CPU-bound work, where the useful level of parallelism is the number of cores, so pools should be sized to the CPU count; for long-running background tasks that occupy a thread for hours; and as the carrier threads that run virtual threads. Each pool should have an explicit size, a name that appears in thread dumps and metrics, and a rejection policy chosen on purpose.
Treat thread counts as part of capacity planning. Every pool in a service, such as request handling, messaging consumers, scheduled jobs and client libraries, adds to the total, and the sum must fit within container memory, the pids limit and the CPU budget. A useful design review question is: "Where does this service block, and which thread waits while it does?" The answer decides whether the service needs larger pools, virtual threads or a change in how it calls its dependencies.
10Interview Questions & Answers
What is a platform thread?
A platform thread is the classic kind of Java thread: a java.lang.Thread object that wraps exactly one operating-system thread for its whole life. The operating system schedules it on CPU cores preemptively, alongside every other thread on the machine. Before Java 21 this was the only kind of thread; now it is called a platform thread to distinguish it from virtual threads, which the JVM schedules onto a small pool of platform threads. Because each platform thread is a real OS thread, it carries OS-level costs and limits, which is why servers pool them and why virtual threads were introduced.
What does a platform thread cost?
Each platform thread reserves a stack, 1 MB by default on 64-bit Linux, which is committed as the stack is used, plus JVM and kernel data structures. Creating one takes tens of microseconds because it involves a system call. Running many of them adds context-switch overhead and cache disruption, and they add work for the JVM, because every thread's stack must be scanned as a GC root and every thread must reach a safepoint during certain operations. One thread is cheap; ten thousand threads mean gigabytes of address space, noticeable scheduling overhead and longer pauses, which is why platform threads are usually pooled.
Which threads does the JVM run besides application threads?
A running JVM has many threads besides the application's own. The VM thread performs operations that need all application threads stopped at a safepoint. Garbage-collection threads do parallel and concurrent GC work, and C1 and C2 compiler threads JIT-compile hot methods. The Reference Handler and Finalizer threads process reference objects, the Cleaner thread runs cleaning actions, the Signal Dispatcher handles OS signals, and the Attach Listener lets tools such as jcmd connect. Applications using virtual threads also have ForkJoinPool carrier threads. A thread dump from jstack or jcmd Thread.print shows all of them, which is useful when diagnosing CPU usage.
What limits the number of platform threads?
Several limits apply at once. The operating system limits threads per user (ulimit -u) and process IDs system-wide (kernel.pid_max and threads-max). In containers, the cgroup pids.max limit, often set through Kubernetes' podPidsLimit, caps processes and threads per pod. Each thread's stack consumes native memory outside the heap, which counts against the container's memory limit. And CPU scheduling overhead grows as runnable threads increase. In practice, a typical service can run a few thousand platform threads before one of these limits or performance degradation stops it, which is far below what many I/O-bound workloads need.
Why do platform threads limit I/O-bound server throughput?
In a thread-per-request server, each in-flight request occupies one platform thread from start to finish, including all the time it spends waiting for a database query or a remote call. If a request spends 95% of its time waiting, a pool of 200 threads can handle only 200 concurrent requests, while the CPUs are mostly idle. Adding more threads helps up to the limits of memory and scheduling overhead, then stops helping. Throughput is therefore capped by the number of threads, not by CPU. Asynchronous I/O and, since Java 21, virtual threads remove that cap by not tying up an OS thread while waiting.
When are platform threads still the right choice in Java 25?
Platform threads are still the right choice in several cases. CPU-bound work, such as image processing or calculations, gains nothing from more threads than cores, so a fixed pool of platform threads sized to the CPU count is ideal. Long-running tasks that occupy a thread continuously, such as a background polling loop, do not benefit from virtual threads. Platform threads are also the carriers that run virtual threads. And some features need real OS threads: thread priorities, native code that relies on thread identity or affinity, and code that holds monitors around long blocking operations, which pinned virtual threads in older JDK versions.