An efficient standard API for stack walking that allows easy filtering and lazy access to stack trace information Credit: Sarabbit 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 and ResourceBundle::getBundle methods use the immediate caller’s classloader to load a class and a resource bundle, respectively. Reflective APIs such as Class::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 the Thread::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 StackWalkers. 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 a StackWalker instance that’s configured to skip all hidden frames and that doesn’t retain any Class reference. StackWalker getInstance(StackWalker.Option option): Return a StackWalker instance with the given option specifying the stack frame information that it can access. StackWalker getInstance(Set<StackWalker.Option> options): Return a StackWalker instance with the given options specifying the stack frame information that it can access. If the given options is empty, this StackWalker is configured to skip all hidden frames and to not retain any Class reference. StackWalker getInstance(Set<StackWalker.Option> options, int estimatedDepth): Return a StackWalker instance with the given options specifying the stack frame information that it can access. If the given options is empty, this StackWalker is configured to skip all hidden frames and to not retain any Class reference. Furthermore, estimatedDepth specifies the estimated number of stack frames that this StackWalker 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 StackFrames. 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 StackFrames 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. Call int 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. Call Class<?> getDeclaringClass() to return this value. This method throws UnsupportedOperationException when this StackWalker isn’t configured with RETAIN_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. Call String 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. Call int 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. download Download the source Get the source code for this post’s applications. Created by Jeff Friesen for JavaWorld 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 Related content news Microsoft unveils imaging APIs for Windows Copilot Runtime Generative AI-backed APIs will allow developers to build image super resolution, image segmentation, object erase, and OCR capabilities into Windows applications. By Paul Krill Nov 19, 2024 2 mins Generative AI APIs Development Libraries and Frameworks feature Designing the APIs that accidentally power businesses Well-designed APIs, even those often-neglected internal APIs, make developers more productive and businesses more agile. By Jean Yang Nov 18, 2024 6 mins APIs Software Development news AI accelerating API development, IBM says Generative AI is helping API teams complete projects faster, while APIs also are fueling the use of AI, company official says. By Paul Krill Nov 07, 2024 2 mins Generative AI APIs Artificial Intelligence news WSO2 API managers manage AI APIs WSO2 API Manager and WSO2 API Platform for Kubernetes now allow developers to manage AI services as APIs, with support for OpenAI, Mistral A,I and Microsoft Azure OpenAI. By Paul Krill Nov 05, 2024 3 mins Generative AI APIs Devops Resources Videos