An efficient standard API for stack walking that allows easy filtering and lazy access to stack trace information
JEP 259: Stack-Walking API defines an efficient standard API for stack walking that allows the easy filtering of and lazy access to stack trace information. This API supports short walks that stop at a stack frame matching given criteria, and also supports long walks that traverse the entire stack. This post introduces you to the Stack-Walking API.
From StackTraceElement to StackWalker
Java 1.4 introduced the java.lang.StackTraceElement
class to describe an element representing a stack frame in a stack trace. This class provides methods that return the fully qualified name of the class containing the execution point represented by this stack trace element along with other useful information. Java 1.4 also introduced the StackTraceElement[] getStackTrace()
method to the java.lang.Thread
and java.lang.Throwable
classes. This method respectively returns an array of stack trace elements representing the invoking thread’s stack dump and provides programmatic access to the stack trace information printed by printStackTrace()
.
There are several reasons why you might want to access stack trace elements. I’ve listed three reasons below — you can probably add to this list:
- Understand an application’s behavior.
- Log stack trace element details to assist with debugging.
- Find out who called a certain method in order to identify the source of a resource leak.
Before Java 9, you might obtain a stack trace by instantiating Throwable
and invoking its getStackTrace()
method, as shown here:
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
Unfortunately, this approach to obtaining a stack trace is rather costly and impacts performance. The Java Virtual Machine (JVM) eagerly captures a snapshot of the entire stack (except for hidden stack frames), even when you only need the first few frames. Also, your code will probably have to process frames that are of no interest, which is also time-consuming. Finally, you cannot access the actual java.lang.Class
instance of the class that declared the method represented by a stack frame. To access this Class
object, you’re forced to extend java.lang.SecurityManager
in order to access the protected getClassContext()
method, which returns the current execution stack as an array of Class
objects.
These APIs don’t satisfy the use cases that depend on the JDK-internal sun.reflect.Reflection::getCallerClass
method, or else their performance overhead is intolerable. These use cases include:
- Walk the stack until the immediate caller’s class is found. Every JDK caller-sensitive API looks up its immediate caller’s class to determine the API’s behavior. For example, the
Class::forName
andResourceBundle::getBundle
methods use the immediate caller’s classloader to load a class and a resource bundle, respectively. Reflective APIs such asClass::getMethod
use the immediate caller’s classloader to determine the security checks to be performed. - Walk the stack, filtering out the stack frames of specific implementation classes to find the first non-filtered frame. The
java.util.logging
API filters intermediate stack frames (typically implementation-specific and reflection frames) to find the caller’s class. - Walk the stack to find all protection domains, until the first privileged frame is reached. This is required in order to do permission checks.
- Walk the entire stack, possibly with a depth limit. This is required to generate the stack trace of any
Throwable
object, and to implement theThread::dumpStack
method.
Java 9 introduces the Stack-Walking API as a more performant and capable alternative to the StackTraceElement
– and SecurityManager
-related APIs. The Stack-Walking API primarily consists of the java.lang.StackWalker
class with its nested Option
class and StackFrame
interface. However, Stack-Walking also includes the java.lang.IllegalCallerException
class.
StackWalker basics
The StackWalker
class is easy to use. In this section, I’ll focus on the basics by showing you first how to obtain a StackWalker
instance and then how to use this instance to walk all or only a few stack frames.
Obtaining a StackWalker
StackWalker
provides four static getInstance()
methods that return StackWalker
s. The methods differ in whether or not the walkers also access hidden frames or refective frames (a subset of hidden frames) and retain Class
references:
StackWalker getInstance()
: Return aStackWalker
instance that’s configured to skip all hidden frames and that doesn’t retain anyClass
reference.StackWalker getInstance(StackWalker.Option option)
: Return aStackWalker
instance with the givenoption
specifying the stack frame information that it can access.StackWalker getInstance(Set<StackWalker.Option> options)
: Return aStackWalker
instance with the givenoptions
specifying the stack frame information that it can access. If the givenoptions
is empty, thisStackWalker
is configured to skip all hidden frames and to not retain anyClass
reference.StackWalker getInstance(Set<StackWalker.Option> options, int estimatedDepth)
: Return aStackWalker
instance with the givenoptions
specifying the stack frame information that it can access. If the givenoptions
is empty, thisStackWalker
is configured to skip all hidden frames and to not retain anyClass
reference. Furthermore,estimatedDepth
specifies the estimated number of stack frames that thisStackWalker
instance will traverse.StackWalker
could use this value as a hint for its buffer size.
The value passed to option
or included in options
is one of StackWalker.Option.RETAIN_CLASS_REFERENCE
, StackWalker.Option.SHOW_HIDDEN_FRAMES
, or StackWalker.Option.SHOW_REFLECT_FRAMES
.
The following examples demonstrate these methods:
import static java.lang.StackWalker.Option.*;
StackWalker sw1 = StackWalker.getInstance();
StackWalker sw2 = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
StackWalker sw3 = StackWalker.getInstance(Set.of(RETAIN_CLASS_REFERENCE,
SHOW_HIDDEN_FRAMES));
StackWalker sw4 = StackWalker.getInstance(Set.of(RETAIN_CLASS_REFERENCE),
16);
The first example skips all hidden frames and doesn’t retain any Class
reference. The second example is like the first example except that it retains Class
references by passing RETAIN_CLASS_REFERENCE
.
The third example also retains Class
references, and shows hidden frames by passing SHOW_HIDDEN_FRAMES
. The final example retains this reference and also sets the estimated traversal depth to 16
. Note that the third and fourth examples demonstrate Java 9’s convenience factory methods enhancement in a java.util.Set
context. (I discussed this enhancement in Part 1 of this series.)
Walking all stack frames with forEach()
Once you have a StackWalker
instance, you can access stack frames by invoking the forEach()
and walk()
methods. The forEach()
method header appears below:
void forEach(Consumer<? super StackWalker.StackFrame> action)
This method walks the stack, performing the given action
on each element of the current thread’s stream of StackFrame
s. Traversal starts at the stack’s top-most frame, which identifies the method that called forEach()
.
Listing 1 presents the source code to an application that demonstrates forEach()
.
Listing 1. SWDemo.java
(version 1)
public class SWDemo
{
public static void main(String[] args)
{
a();
}
public static void a()
{
b();
}
public static void b()
{
c();
}
public static void c()
{
StackWalker sw = StackWalker.getInstance();
sw.forEach(System.out::println);
}
}
main()
starts a chain of method invocations. The final invocation instantiates StackWalker
and, on this object, invokes forEach()
with a System.out::println
method reference to print out all stack frames.
Compile Listing 1 as follows:
javac SWDemo.java
Run the resulting application as follows:
java SWDemo
You should observe the following output — the first line identifies the top stack frame:
SWDemo.c(SWDemo.java:21)
SWDemo.b(SWDemo.java:15)
SWDemo.a(SWDemo.java:10)
SWDemo.main(SWDemo.java:5)
Walking all or fewer stack frames with walk()
It’s often the case that you’ll want to limit the number of stack frames that are walked, for performance or another reason. StackWalker
provides the walk()
generic method for this task:
<T> T walk(Function<? super Stream<StackWalker.StackFrame>, ? extends T> function)
walk()
opens a sequential stream of StackFrame
s for the current thread and then applies the given function
to walk the StackFrame
stream. The stream reports stack frames in order, from the top-most frame that represents the execution point at which the stack was generated (and which identifies the method that called walk()
) to the bottom-most frame. walk()
returns the type of java.util.function.Function
‘s return value (the R
in Function<T,R>
).
Listing 2 presents the source code to an application that demonstrates walk()
.
Listing 2. SWDemo.java
(version 2)
import java.util.List;
import java.util.stream.Collectors;
public class SWDemo
{
public static void main(String[] args)
{
a();
}
public static void a()
{
b();
}
public static void b()
{
c();
}
public static void c()
{
StackWalker sw = StackWalker.getInstance();
List<StackWalker.StackFrame> frames;
frames = sw.walk(frames_ -> frames_.collect(Collectors.toList()));
frames.forEach(System.out::println);
System.out.println();
long numFrames = sw.walk(frames_ -> frames_.count());
System.out.printf("Total number of frames: %d%n%n", numFrames);
frames = sw.walk(frames_ -> frames_.limit(2).collect(Collectors.toList()));
frames.forEach(System.out::println);
}
}
c()
instantiates StackWalker
and then uses this object to walk all stack frames (equivalent to forEach()
), count all stack frames (count()
returns a long
), and walk only the first two stack frames. The following output is generated:
SWDemo.c(SWDemo.java:27)
SWDemo.b(SWDemo.java:19)
SWDemo.a(SWDemo.java:14)
SWDemo.main(SWDemo.java:9)
Total number of frames: 4
SWDemo.c(SWDemo.java:34)
SWDemo.b(SWDemo.java:19)
StackWalker advanced
Having mastered the basics of StackWalker
, it’s time for you to pursue more advanced topics. We’ll begin by considering why the walk()
method was designed to receive a function instead of return a stream.
Understanding a design curiosity
Each thread has its own execution stack. You might think of this stack as a stable data structure that the JVM modifies only at the top, by adding or removing a single frame each time a method is entered or exited. In reality, the JVM can restructure a thread’s stack any time it sees fit, to improve performance.
For a stack walker to observe a consistent stack, it must make certain that the stack is stable while building stack frames. This can only happen when the stack walker controls the stack, which occurs while walk()
is executing. As a result, stream processing must occur during the call, and so the stream cannot be returned.
If you return the stream by passing an identity function to walk()
, which I demonstrate below, you’ll be rewarded with a thrown java.lang.IllegalStateException
object when you try to process the stream:
Stream<StackWalker.StackFrame> dangerousSW = stackWalker.walk(frames -> frames);
dangerousSW.count(); // IllegalStateException is thrown.
Obtaining the caller class
StackWalker
supports the concept of a caller class, which the JDK 9 documentation defines as the Class
object of the caller who invoked the method that invoked StackWalker
‘s Class<?> getCallerClass()
method. This method is the replacement for sun.reflect.Reflection.getCallerClass()
, which may not be available in future Java releases.
In its search for the caller class, getCallerClass()
disregards reflection frames, method handles, and hidden frames regardless of the SHOW_REFLECT_FRAMES
and SHOW_HIDDEN_FRAMES
options with which this StackWalker
object has been configured.
The getCallerClass()
method returns the caller’s Class
object when successful and throws an exception when there’s a problem. It throws java.lang.UnsupportedOperationException
when the invoking StackWalker
isn’t configured with the RETAIN_CLASS_REFERENCE
option. It throws IllegalCallerException
when there is no caller frame; in other words, when this getCallerClass()
method is called from a method that’s associated with the bottom-most stack frame.
Listing 3 presents the source code to an application that demonstrates getCallerClass()
.
Listing 3. SWDemo.java
(version 3)
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
public class SWDemo
{
public static void main(String[] args)
{
a();
Class<?> cc = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
.getCallerClass();
System.out.println(cc);
}
public static void a()
{
b();
}
public static void b()
{
c();
}
public static void c()
{
Class<?> cc = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
.getCallerClass();
System.out.println(cc);
}
}
main()
first invokes a()
, which results in c()
obtaining and outputting the caller class. Then main()
attempts to obtain the caller class from the bottom-most stack frame, resulting in an exception:
class SWDemo
Exception in thread "main" java.lang.IllegalStateException: no caller frame
at java.base/java.lang.StackStreamFactory$CallerClassFinder.consumeFrames(StackStreamFactory.java:687)
at java.base/java.lang.StackStreamFactory$CallerClassFinder.consumeFrames(StackStreamFactory.java:610)
at java.base/java.lang.StackStreamFactory$AbstractStackWalker.doStackWalk(StackStreamFactory.java:304)
at java.base/java.lang.StackStreamFactory$AbstractStackWalker.callStackWalk(Native Method)
at java.base/java.lang.StackStreamFactory$AbstractStackWalker.beginStackWalk(StackStreamFactory.java:368)
at java.base/java.lang.StackStreamFactory$AbstractStackWalker.walk(StackStreamFactory.java:241)
at java.base/java.lang.StackStreamFactory$CallerClassFinder.findCaller(StackStreamFactory.java:668)
at java.base/java.lang.StackWalker.getCallerClass(StackWalker.java:541)
at SWDemo.main(SWDemo.java:9)
I’m surprised to discover IllegalStateException
instead of IllegalCallerException
. Perhaps this situation has been remedied in a later build of JDK 9.
What’s in a StackFrame?
forEach()
and walk()
provide access to StackFrame
objects. Each StackFrame
object represents a method invocation and provides access to the following information:
- Bytecode index: This is the index of the current bytecode instruction relative to the start of the method. More technically, it’s the index into the code array of the
Code
attribute containing the execution point represented by this stack frame. Callint getByteCodeIndex()
to return this value. - Class name: This is the binary name of the declaring class of the called method represented by this stack frame. Call
String getClassName()
to return this value. - Declaring class: This is the
Class
object of the class declaring the called method. CallClass<?> getDeclaringClass()
to return this value. This method throwsUnsupportedOperationException
when thisStackWalker
isn’t configured withRETAIN_CLASS_REFERENCE
. - File name: This is the name of the source file containing the execution point represented by this stack frame. This name generally corresponds to the
SourceFile
attribute of the relevant class file. CallString getFileName()
to return this value. - Is native: This is an indicator of whether the method is native (true) or not native (false). Call
boolean isNativeMethod()
to return this value. - Line number: This is the number of the source line containing the execution point represented by this stack frame. It’s typically derived from the
LineNumberTable
attribute of the relevant class file. Callint getLineNumber()
to return this value. - Method name: This is the name of the called method, which is represented by this stack frame. Call
String getMethodName()
to return this value.
Listing 4 presents the source code to an application that demonstrates these methods.
Listing 4. SWDemo.java
(version 4)
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
public class SWDemo
{
public static void main(String[] args)
{
a();
}
public static void a()
{
b();
}
public static void b()
{
c();
}
public static void c()
{
StackWalker sw = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
sw.forEach(f->
{
System.out.printf("Bytecode index: %d%n",
f.getByteCodeIndex());
System.out.printf("Class name: %s%n",
f.getClassName());
System.out.printf("Declaring class: %s%n",
f.getDeclaringClass());
System.out.printf("File name: %s%n",
f.getFileName());
System.out.printf("Is native: %b%n",
f.isNativeMethod());
System.out.printf("Line number: %d%n",
f.getLineNumber());
System.out.printf("Method name: %s%n%n",
f.getFileName());
});
}
}
Listing 4 is similar to Listing 1 except that the StackWalker
is configured with RETAIN_CLASS_REFERENCE
and various StackFrame
methods are called. You should observe the following output:
Bytecode index: 13
Class name: SWDemo
Declaring class: class SWDemo
File name: SWDemo.java
Is native: false
Line number: 23
Method name: SWDemo.java
Bytecode index: 0
Class name: SWDemo
Declaring class: class SWDemo
File name: SWDemo.java
Is native: false
Line number: 17
Method name: SWDemo.java
Bytecode index: 0
Class name: SWDemo
Declaring class: class SWDemo
File name: SWDemo.java
Is native: false
Line number: 12
Method name: SWDemo.java
Bytecode index: 0
Class name: SWDemo
Declaring class: class SWDemo
File name: SWDemo.java
Is native: false
Line number: 7
Method name: SWDemo.java
StackFrame
also declares a StackTraceElement toStackTraceElement()
method to convert a StackFrame
to a StackTraceElement
. You’ll need to use this method to obtain classloader and module information.
Demonstrating StackWalker options
I’ve previously referred to the RETAIN_CLASS_REFERENCE
, SHOW_HIDDEN_FRAMES
, and SHOW_REFLECT_FRAMES
StackWalker
options. Listing 5 presents the source code to an application that demonstrates these options.
Listing 5. SWDemo.java
(version 5)
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.lang.StackWalker.Option.*;
public class SWDemo
{
public static void main(String[] args) throws Exception
{
// 1. Demonstrate what happens when RETAIN_CLASS_REFERENCE is missing.
try
{
StackWalker sw = StackWalker.getInstance();
sw.forEach(f -> System.out.printf("Declaring class: %s%n",
f.getDeclaringClass()));
}
catch (UnsupportedOperationException uoe)
{
System.out.printf("Unsupported operation: %s%n", uoe.getMessage());
}
System.out.println();
// 2. Demonstrate what happens when RETAIN_CLASS_REFERENCE is present.
StackWalker sw = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
sw.forEach(f -> System.out.printf("Declaring class: %s%n",
f.getDeclaringClass()));
System.out.println();
// 3. Demonstrate what happens when SHOW_REFLECT_FRAMES and
// SHOW_HIDDEN_FRAMES are absent.
invokeReflectively(() -> StackWalker.getInstance()
.forEach(System.out::println));
System.out.println();
// 4. Demonstrate what happens when SHOW_REFLECT_FRAMES is
// present.
invokeReflectively(() -> StackWalker.getInstance(SHOW_REFLECT_FRAMES)
.forEach(System.out::println));
System.out.println();
// 5. Demonstrate what happens when SHOW_HIDDEN_FRAMES is
// present.
invokeReflectively(() -> StackWalker.getInstance(SHOW_HIDDEN_FRAMES)
.forEach(System.out::println));
}
public static void invokeReflectively(Runnable r) throws Exception
{
SWDemo.class.getMethod("run", Runnable.class).invoke(null, r);
}
public static void run(Runnable r)
{
r.run();
}
}
Listing 5 includes invokeReflectively()
and run()
methods to create a more complex stack so that I can demonstrate the SHOW_REFLECT_FRAMES
and SHOW_HIDDEN_FRAMES
options. The following output is generated:
Unsupported operation: No access to RETAIN_CLASS_REFERENCE: []
Declaring class: class SWDemo
SWDemo.lambda$main$2(SWDemo.java:37)
SWDemo.run(SWDemo.java:61)
SWDemo.invokeReflectively(SWDemo.java:56)
SWDemo.main(SWDemo.java:36)
SWDemo.lambda$main$3(SWDemo.java:44)
SWDemo.run(SWDemo.java:61)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:543)
SWDemo.invokeReflectively(SWDemo.java:56)
SWDemo.main(SWDemo.java:43)
SWDemo.lambda$main$4(SWDemo.java:51)
SWDemo$$Lambda$72/1525262377.run(Unknown Source)
SWDemo.run(SWDemo.java:61)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.base/java.lang.reflect.Method.invoke(Method.java:543)
SWDemo.invokeReflectively(SWDemo.java:56)
SWDemo.main(SWDemo.java:50)
The first output section reveals that UnsupportedOperationException
was thrown because the StackWalker
wasn’t configured with RETAIN_CLASS_REFERENCE
before calling getDeclaringClass()
. The second output section reveals no problem following proper configuration.
The third output section doesn’t include reflection frames or other hidden frames because the StackWalker
wasn’t configured with SHOW_REFLECT_FRAMES
or SHOW_HIDDEN_FRAMES
. Following proper configuration, the fourth section shows the reflection frames and the fifth section shows the reflection and other hidden frames.
Performance considerations
The Stack-Walking API was designed to be more performant than the StackTraceElement
-based alternative. This API is more performant as long as you don’t instantiate StackTraceElement
, which is expensive time-wise, and which is instantiated by StackFrame
‘s getFileName()
, getLineNumber()
, and toStackTraceElement()
methods; and also by the default StackFrame
implementation’s toString()
method.
StackWalker
is faster than StackTraceElement
when capturing the full stack, but performance can be further improved by reducing the number of recovered frames with Stream::limit
. StackWalker
‘s walk()
method lazily evaluates stack frames, and limit
reduces the number of stack frames that are recovered. In contrast, using Stream::skip
to skip stack frames offers no performance benefit because the StackWalker
still has to walk past skipped stack frames.
Conclusion
Java 9’s Stack-Walking API facilitates access to the current execution stack via simple expressions such as StackWalker.getInstance().walk(frames -> /* ... */);
. By default, a stack walker excludes hidden and reflection frames for performance reasons, but you can override that behavior. It also supports obtaining the caller class, and stack frame information (such as the declaring class and bytecode index). This API is more performant than the older StackTraceElement
-based API, provided that you avoid instantiating StackTraceElement
. Note that you can improve performance by reducing the number of recovered stack frames with Stream::limit
.
The following software was used to develop the post’s code:
- 64-bit JDK 9ea+154
The post’s code was tested on the following platform(s):
- JVM on 64-bit Windows 8.1