Java 9's other new enhancements, Part 5: Stack-Walking API

An efficient standard API for stack walking that allows easy filtering and lazy access to stack trace information

Page 2 of 2

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
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
| 1 2 Page 2