How to profile threads in Java 5:
putting getThreadInfo() in a loop

On the previous page, we described two important calls to ThreadMXBean: getAllThreadIds() and getThreadInfo(). Now we'll start to put these together in a background loop that will profile our server or application. Our profiler will sit inside a Thread of its own (we can actually define a Profiler class that extends Thread) and its run() method will look something like this:

private volatile terminateRequest;
private final long profileInterval = 200;
private final int maxDepth = 5;

public void run() {
  ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
  try {
    while (!terminateRequest) {
      long[] threadIds = threadBean.getAllThreadIds();
      ThreadInfo[] infs = threadBean.getThreadInfo(threadIds, maxDepth);

      for (ThreadInfo inf : infs) {
        StackTraceElement[] str = inf.getStackTrace();
        if (str == null)
          continue;
        StackTraceElement el = mostInterestingElement(str);
        if (el != null)
          incrementCountOf(el);
      }

      Thread.sleep(profileInterval);
    }
  } catch (InterruptedException iex) {
    Thread.currentThread().interrupted();
    throw new RuntimeException("Profiler interrupted");
  }
}

This skeleton gives us a schema for periodically getting the stack traces of all live threads, and for each stack trace incrementing the count of the "most interesting element": that is, the line in the stack trace that for our purposes tells us "where the thread is". For now, we haven't considered synchronization issues (in a separate thread, we may want to read the profile data collected!); we'll come back to that in a moment. For deciding of a stack trace which is the "most interesting" line that we want to count, then in the very simplest case, we could take the top element. A slight improvement might be to take the top element for which a line number is available:

private StackTraceElement mostInterestingElement(StackTraceElement[] st) {
  for (int n = st.length,i=0; i < n; i++) {
    StackTraceElement el = st[i];
    if (el.getLineNumber() >= 0)
      return el;
  }
  return null;
}

Now for our counts. The simplest thing to implement is to take a count of number of times a thread is at a particular class/method/line combination, as this is the information encapsulated by StackTraceElement. The StackTraceElement handily implements hashCode() and equals() for us, so we can create a simple hash map:

private static class ProfileInfo {
  private int tickCount;
}

private Map<StackTraceElement,ProfileInfo> lineCounts =
  new HashMap<StackTraceElement,ProfileInfo>(500);

private void incrementCountOf(StackTraceElement el) {
  ProfileInfo inf = lineCounts.get(el);
  if (inf == null) {
    lineCounts.put(el, inf = new ProfileInfo());
  }
  inf.tickCount++;
}

I've defined a ProfileInfo class to hold our counts. If we want a simple number, we could just use a plain old Integer; creating our own class means we can potentially add other information such as CPU time if we want. (It also avoids the ever so slight overhead of creating a new object on each update which would be the case if we used Integer: although in any case, creating one new object per thread on each iteration may be of minimal cost compared to that of actually getting the thread stacks.)

After incrementing the corresponding counts for all the threads, we sleep for a specified interval: in this example, roughly 200 milliseconds or 5 polls per second. This interval is generally suitable for a moderately busy server. The sleep interval is always approximate. The Thread.sleep() method, like methods generally that put a thread in a wait state, can be interrupted and throw an InterruptedException. (See the section on Java threads and in particular the section on the behaviour of Thread.sleep().) If that happened, there'd probably be nothing elegant we wanted to do, but we're obliged to catch it because run() can't throw a checked exception. So we simply re-cast it as an unchecked RuntimeException.

If we put all the above together, we end up with a simple profiler that sits dutifully gathering profile information. What it doesn't do is provide us with a way to query the profiling information, which will entail some synchronization issues while profiling.


If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.

Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.