[Hacking the JVM] Decoding Java Thread Dumps

We’ve all used Java thread dumps, but how are the values we see captured internally? What do those fields really mean? In this post, as part of my “Hacking the JVM” series, I’ll take a look under the hood of the JDK 17 source code. We’ll decode the mystery behind key thread dump fields, understanding exactly how the JVM tracks and reports on its threads.

"Monitor Ctrl-Break" #19 daemon prio=5 os_prio=31 cpu=18.48ms elapsed=4.22s tid=0x0000000139809600 nid=28419 runnable  [0x000000016f8b2000]
   java.lang.Thread.State: RUNNABLE
	at sun.nio.ch.SocketDispatcher.read0(java.base@21.0.6/Native Method)
	at sun.nio.ch.SocketDispatcher.read(java.base@21.0.6/SocketDispatcher.java:47)

	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.lang.Thread.run(Thread.java:834)

Let’s break down each field

Field NameDescription
“Monitor Ctrl-Break” Name of the Thread
#19Java Thread id
daemonOptionally present, if daemon
prio=5Java level priority
os_prio=31OS Level Priority
cpu=18.48msCPU time used by thread
elapsed=4.22sWall time since thread started
tid=0x00007f8c0c567800Thread Address
nid=0x9abcNative Thread id

A rough attempt at sketching the understanding

Let’s dive deeper into the each field

Thread Name

The first quoted string (“Monitor Ctrl-Break”) is the thread’s name. You can give names explicitly when constructing a Thread or later using setName().

Source

public Thread(String name) {
   this(null, null, name, 0);
}

Practical Tip:

Meaningful thread names make debugging much easier (e.g. rpcpool-worker-3 instead of Thread-42)

What happens if you don’t assign a name?

If no name is provided, a thread is given a name like “Thread-”<sequenceNumber>.

This sequence number is a comes from within the Thread class, which is incremented each time it is called.
Source

private static int threadInitNumber;

private static int threadInitNumber;
private static synchronized int nextThreadNum() {
   return threadInitNumber++;
}

Java Thread Id

The number after # is the Java Thread id. This is a monotonically increasing sequence incremented for each Thread created since the beginning of the JVM.

It’s a simple counter increment in Thread.java

Source

/* For generating thread ID */
private static long threadSeqNumber;

private static synchronized long nextThreadID() {
   return ++threadSeqNumber;
}

So #19 means this was the 19th thread created since the JVM started.

Daemon status

If a thread is set to a daemon, daemon is displayed, else it is omitted.

Java Thread Priority (prio)

This is the priority you can set via Thread.setPriority(1-10). By default, it’s 5. It’s just a hint to the JVM and OS, not a hard guarantee.

OS Thread Priority (os_prio)

This is the thread’s scheduling priority at the native layer. HotSpot uses mapping tables (java_to_os_priority[]) to translate Java’s 1–10 values into OS scheduler values.
The printed os priority comes from OS specific class. For example, following is snippet for Linux

Source

OSReturn os::get_native_priority(const Thread* const thread,
                                 int *priority_ptr) {
  if (!UseThreadPriorities || ThreadPriorityPolicy == 0) {
    *priority_ptr = java_to_os_priority[NormPriority];
    return OS_OK;
  }

  errno = 0;
  *priority_ptr = getpriority(PRIO_PROCESS, thread->osthread()->thread_id());
  return (*priority_ptr != -1 || errno == 0 ? OS_OK : OS_ERR);
}

CPU

This is the cumulative CPU time consumed by this thread, measured by the OS. The unit is milliseconds

Source

st->print("cpu=%.2fms ",
         os::thread_cpu_time(const_cast<Thread*>(this), true) / 1000000.0
         );

Practical tip

Compare CPU vs elapsed to spot CPU‑hungry threads. If elapsed is large but CPU is near zero, that thread has mostly been idle

Elapse time

This is the wall‑clock time since the thread started, not since JVM startup.

HotSpot records the thread’s start time and then at dump time prints the difference from current time.

Source

st->print("elapsed=%.2fs ",
         _statistical_info.getElapsedTime() / 1000.0

Thread Address (tid)

Despite the name, this isn’t a “thread ID”. It’s actually the memory address of the JavaThread structure inside HotSpot. The dump prints it by converting the pointer to a number.

Practical tip: This is the field you can use to correlate a JVM thread with what the operating system reports.

Source 

P2i is present in globalDefinitions.hpp

st->print("tid=" INTPTR_FORMAT " ", p2i(this));

// Convert pointer to intptr_t, for use in printing pointers.
inline intptr_t p2i(const volatile void* p) {
 return (intptr_t) p;
}

Native Thread Id (nid)

This field indicates the native thread id or kernel thread id in hex. On Linux, it’s the value you’d also see from ps -L or top -H. It comes from the native OSThread object where HotSpot caches the real OS ID. The function also prints additional information about the state.

void OSThread::print_on(outputStream *st) const {
 st->print("nid=0x%x ", thread_id());
 switch (_state) {
   case ALLOCATED:               st->print("allocated ");                 break;
   case INITIALIZED:             st->print("initialized ");               break;
   case RUNNABLE:                st->print("runnable ");                  break;
   case MONITOR_WAIT:            st->print("waiting for monitor entry "); break;
   case CONDVAR_WAIT:            st->print("waiting on condition ");      break;
   case OBJECT_WAIT:             st->print("in Object.wait() ");          break;
   case BREAKPOINTED:            st->print("at breakpoint");               break;
   case SLEEPING:                st->print("sleeping");                    break;
   case ZOMBIE:                  st->print("zombie");                      break;
   default:                      st->print("unknown state %d", _state); break;
 }
}

Wrapping Up

That was my walk‑through where each thread dump field comes from in JDK 17.
At first, these numbers seemed mysterious; but once I dug into the HotSpot source, I found each one has a straightforward place inside the JVM.💡 Trivia: Not all threads in a dump have a Java ID (#id). Why? That’s something I’ll explore in a future post as I continue hacking the JVM.

Leave a Reply

Your email address will not be published. Required fields are marked *