A stack trace is a list of method calls arranged in such a way that the method at which an exception was thrown will be at the top followed by the caller to that method and so on till the main method of the program. We can print a stack trace to the console by invoking printStacktrace() on the exception caught in the catch block of a try-catch statement.
While developing Java-based applications, sometimes we would want to traverse the execution stack to analyze what is going on at runtime. This would be required in situations where we need to trace why there is a memory leak or excessive CPU consumption, why a particular exception was thrown during runtime in the application logs, etc.
Getting Current Execution Stack Before Java 9
Prior to Java 9, if someone wanted to get this information, he/she would invoke the getStackTrace() on a Throwable instance. Let us see how it was done with an example:
1 2 3 4 5 6 7 8 9 10 |
package com.javatutorials.runtime; public class StackTraceExample { public static void main(String[] args) { Sums sum = new Sums(); sum.addInts(3, 4); } } |
1 2 3 4 5 6 7 8 9 |
package com.javatutorials.runtime; public class Sums { public int addInts(int a, int b){ StackTraceUtility.getStackTrace(); return a+b; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.javatutorials.runtime; public class StackTraceUtility { public static void getStackTrace(){ StackTraceElement[] traces = new Throwable().getStackTrace(); int count = 1; for(StackTraceElement e: traces){ System.out.println(" Trace number : " + count++); System.out.println(" at Class : " + e.getClassName() + " Method: " + e.getMethodName()+" Line number : " + e.getLineNumber()); } } } |
Output for the above code will be as follows:
1 2 3 4 5 6 |
Trace number : 1 at Class : com.javatutorials.runtime.StackTraceUtility method: getStackTrace line number : 6 Trace number : 2 at Class : com.javatutorials.runtime.Sums method: addInts line number : 6 Trace number : 3 at Class : com.javatutorials.runtime.StackTraceExample method: main line number : 7 |
In the above example, we saw how to use getStackTrace() for getting class name, method name, and line number. But if we want to get the Class objects of each class in the execution stack, we will have to create a subclass of java.lang.SecurityManager and invoke the protected method getClassContext() from that subclass.
There are a few problems while following this approach:
- First of all, it impacts performance and will not contain all details that we want because the snapshot thus produced is that of the entire stack, but will not contain hidden frames.
- We need to examine all the frames in the process, which is, again a costly approach.
- The specification for getStackTrace() method added to java.lang.Thread and java.lang.Throwable allows the JVM implementation to return partial information for the method call. So getStackTrace() method may return partial stack information as a result of optimization of getStackTrace() in the JVM implementation. This partial stack information may not be desirable in situations where we need the full stack information.
The Stack-Walking API
Java 9 (JEP 259) introduced java.lang.StackWalker class as an alternative to SecurityManager.getClassContext() and Thread.getStackTrace() or Throwable.getStackTrace(). The StackWalker class addresses most of the problems faced while using the old methods as discussed earlier. Unlike getStackTrace() which will be giving StackTraceElement array per thread, a single StackWalker can be used by multiple threads for traversing their own stack as it is thread-safe.
There are a nested enum and a nested interface available inside StackWalker class:
- StackWalker.StackFrame – This is the nested interface inside StackWalker class. The obtained object represents a method invocation by StackWalker. From this StackFrame object, it is possible to get information like the file, class, method name and even line number and the corresponding StackTraceElement object for the particular stack frame.
- StackWalker.Option – This is the enum available that denotes which all information of the stack shall be accessed. This enum value is used to get a customized StackWalker object which will be discussed in next subsection.
Getting A StackWalker Instance
There are four overloaded getInstance() methods of StackWalker that can be used to get a StackWalker object:
- getInstance() – returns a StackWalker instance.
- getInstance( StackWalker.Option option ) – returns a StackWalker instance which contains the stack frame information according to the specified enum value.
- getInstance( Set[StackWalker.Option] optionsSet ) – returns a StackWalker instance which contains the stack frame information according to the set of specified Option enum values. In case the optionSet is empty, the returned StackWalker will not contain hidden frames or class references.
- getInstance( Set[StackWalker.Option] optionsSet, int estimateDepth ) – similar to the previous method mentioned. This returns a StackWalker instance which contains stack frame information according to the optionSet values. The estimateDepth value is used to tell how many numbers of stack frames the instance shall traverse. If the estimateDepth is less than or equal to zero, this method will throw an IllegalArgumentException.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
package com.javatutorials.stackwalk; import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; public class StackWalkerDemo { private static final String PATH = System.getProperty("user.dir") + File.separator + "wonder.html"; public static void main(String[] args) { method1(); } private static void method1() { method2(); } private static void method2() { method3(); } private static void method3() { try { method4(); } catch (FileNotFoundException e) { testWalker(); } } private static void method4() throws FileNotFoundException { BufferedReader bufReader = new BufferedReader(new FileReader(PATH)); try (bufReader) { String input; while ((input = bufReader.readLine()) != null) { System.out.println(input); } } catch (IOException e) { e.printStackTrace(); } } private static void testWalker() { StackWalker w1 = StackWalker.getInstance(); System.out.println(" Default getInstance will give the following frames: "); w1.forEach(System.out::println); StackWalker w2 = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); System.out.println(" getInstance with single Option gives the following frames:"); w2.forEach(System.out::println); Class<?> classRef2 = w2.getCallerClass(); System.out.println("Caller class of second StackWalker: " + classRef2.getName()); // Enabling the following will throw exception. // Class<?> classRef1 = w1.getCallerClass(); // System.out.println("Caller class of first StackWalker: " + classRef1.getName()); } } |
In the above class, testWalker() is called from method3(). So, there will not be any stack frame corresponding to method4() in the StackWalker instance. The method4() will throw a FileNotFoundException as the file mentioned in PATH is not created yet. Note that we have created two instances of StackWalker and printed them. This is to show how RETAIN_CLASS_REFERENCE Option is used.
Output for the above will be as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Default getInstance will give the following frames: com.javatutorials.stackwalk.StackWalkerDemo.testWalker(StackWalkerDemo.java:58) com.javatutorials.stackwalk.StackWalkerDemo.method3(StackWalkerDemo.java:35) com.javatutorials.stackwalk.StackWalkerDemo.method2(StackWalkerDemo.java:27) com.javatutorials.stackwalk.StackWalkerDemo.method1(StackWalkerDemo.java:22) com.javatutorials.stackwalk.StackWalkerDemo.main(StackWalkerDemo.java:17) getInstance with single Option gives the following frames: com.javatutorials.stackwalk.StackWalkerDemo.testWalker(StackWalkerDemo.java:61) com.javatutorials.stackwalk.StackWalkerDemo.method3(StackWalkerDemo.java:35) com.javatutorials.stackwalk.StackWalkerDemo.method2(StackWalkerDemo.java:27) com.javatutorials.stackwalk.StackWalkerDemo.method1(StackWalkerDemo.java:22) com.javatutorials.stackwalk.StackWalkerDemo.main(StackWalkerDemo.java:17) Caller class of second StackWalker: com.javatutorials.stackwalk.StackWalkerDemo |
If we uncomment the last two lines, it will throw UnsupportedOperationException as the object w1 was obtained without explicitly specifying RETAIN_CLASS_REFERENCE option. The output will look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
Default getInstance will give the following frames: com.javatutorials.stackwalk.StackWalkerDemo.testWalker(StackWalkerDemo.java:58) com.javatutorials.stackwalk.StackWalkerDemo.method3(StackWalkerDemo.java:35) com.javatutorials.stackwalk.StackWalkerDemo.method2(StackWalkerDemo.java:27) com.javatutorials.stackwalk.StackWalkerDemo.method1(StackWalkerDemo.java:22) com.javatutorials.stackwalk.StackWalkerDemo.main(StackWalkerDemo.java:17) getInstance with single Option gives the following frames: com.javatutorials.stackwalk.StackWalkerDemo.testWalker(StackWalkerDemo.java:61) com.javatutorials.stackwalk.StackWalkerDemo.method3(StackWalkerDemo.java:35) com.javatutorials.stackwalk.StackWalkerDemo.method2(StackWalkerDemo.java:27) com.javatutorials.stackwalk.StackWalkerDemo.method1(StackWalkerDemo.java:22) com.javatutorials.stackwalk.StackWalkerDemo.main(StackWalkerDemo.java:17) Caller class of second StackWalker: com.javatutorials.stackwalk.StackWalkerDemo Exception in thread "main" java.lang.UnsupportedOperationException: This stack walker does not have RETAIN_CLASS_REFERENCE access at java.base/java.lang.StackWalker.getCallerClass(StackWalker.java:551) at com.javatutorials.stackwalk.StackWalkerDemo.testWalker(StackWalkerDemo.java:64) at com.javatutorials.stackwalk.StackWalkerDemo.method3(StackWalkerDemo.java:35) at com.javatutorials.stackwalk.StackWalkerDemo.method2(StackWalkerDemo.java:27) at com.javatutorials.stackwalk.StackWalkerDemo.method1(StackWalkerDemo.java:22) at com.javatutorials.stackwalk.StackWalkerDemo.main(StackWalkerDemo.java:17) |
Using StackWalker.StackFrame
As discussed in the section ‘The StackWalker API,’ the nested interface StackFrame can be used to get more information about the execution stack.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.javatutorials.stackwalk; public class StackFrameDemo { public static void main(String[] args) { method1(); } private static void method1() { method2(); } private static void method2() { Sum sum = new Sum(); int ans = sum.add(9,5); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.javatutorials.stackwalk; import static java.lang.StackWalker.Option.*; public class Sum { public int add(int a, int b) { StackWalker sw = StackWalker.getInstance(RETAIN_CLASS_REFERENCE); sw.forEach(stackFrame -> { System.out.println(" \n*******************************************\n"); System.out.println(" Class name : " + stackFrame.getClassName()); System.out.println(" Declaring Class name : " + stackFrame.getDeclaringClass()); System.out.println(" File name : " + stackFrame.getFileName()); System.out.println(" Bytecode index : " + stackFrame.getByteCodeIndex()); System.out.println(" Line number : " + stackFrame.getLineNumber()); System.out.println(" Method name : " + stackFrame.getMethodName()); System.out.println(" Is method native or not? : " + stackFrame.isNativeMethod()); }); return a+b; } } |
In the above code we use methods from each StackFrame object to get more information about that frame. Output for the above code will be as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
******************************************* Class name : com.javatutorials.stackwalk.Sum Declaring Class name : class com.javatutorials.stackwalk.Sum File name : Sum.java Bytecode index : 13 Line number : 7 Method name : add Is method native or not? : false ******************************************* Class name : com.javatutorials.stackwalk.StackFrameDemo Declaring Class name : class com.javatutorials.stackwalk.StackFrameDemo File name : StackFrameDemo.java Bytecode index : 12 Line number : 18 Method name : method2 Is method native or not? : false ******************************************* Class name : com.javatutorials.stackwalk.StackFrameDemo Declaring Class name : class com.javatutorials.stackwalk.StackFrameDemo File name : StackFrameDemo.java Bytecode index : 0 Line number : 11 Method name : method1 Is method native or not? : false ******************************************* Class name : com.javatutorials.stackwalk.StackFrameDemo Declaring Class name : class com.javatutorials.stackwalk.StackFrameDemo File name : StackFrameDemo.java Bytecode index : 0 Line number : 6 Method name : main Is method native or not? : false |
Note that in the above program, if the StackWalker is not created using RETAIN_CLASS_REFERENCE option, it will throw an UnsupportedOperationException while invoking getDeclaringClass().
Partial Traversal Of Stack Using walk() Method
In a large project, if we are invoking the StackWalker, there will be many frames in the execution stack. As developers, we may want to traverse only a part of the stack instead of the full stack. It is possible to limit how many StackFrames are inspected using the walk() method. Using filter(), skip(), limit() etc, we have multiple ways to filter the stack frames according to our need.
1 2 3 4 5 6 7 8 9 10 |
package com.javatutorials.stackwalk; public class WalkingTheStack { public static void main(String[] args) { Messenger msg = new Messenger(); msg.getResponse(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
package com.javatutorials.stackwalk; import java.lang.StackWalker.Option; import java.lang.StackWalker.StackFrame; import java.util.List; import java.util.Scanner; import java.util.stream.Collectors; public class Messenger { public void getResponse() { sayHello(); } private void sayHello() { Scanner sc = new Scanner(System.in); System.out.println("Enter your name"); String name = sc.next(); printHello(name); } private void printHello(String name) { System.out.println("Hello, " + name + "! Welcome to StackWalker Tutorial."); /** * limit(2) limits the frames to two. Thus, the framesList will contain * only the top two frames of the execution stack */ System.out.println("******Printing the partial stack after limiting******"); List<StackFrame> framesList = StackWalker.getInstance( Option.RETAIN_CLASS_REFERENCE ) .walk( stream -> stream.limit(2).collect( Collectors.toList() ) ); framesList.forEach(System.out::println); /** * skip(2) in the code below skips the top two frames and returns the remaining to framesList */ System.out.println("******Printing the partial stack after skipping two frames******"); framesList = StackWalker.getInstance( Option.RETAIN_CLASS_REFERENCE ) .walk( stream -> stream.skip(2).collect( Collectors.toList() ) ); framesList.forEach(System.out::println); /** * the code below shows how to limit the frames by a particular class name. * To achieve this, we use filter method. */ System.out.println("******Partial Stack with Filtered Frames by Class******"); System.out.println(" Filtered By : " + WalkingTheStack.class.getName()); List<Class> classFilter = List.of(WalkingTheStack.class); framesList = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk( s -> s.filter(f -> classFilter.contains(f.getDeclaringClass())).collect(Collectors.toList())); framesList.forEach(System.out::println); } } |
Output of the above code will be as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
Enter your name Smith Hello, Smith! Welcome to StackWalker Tutorial. ******Printing the partial stack after limiting****** com.javatutorials.stackwalk.TheMessenger.printHello(TheMessenger.java:35) com.javatutorials.stackwalk.TheMessenger.sayHello(TheMessenger.java:23) ******Printing the partial stack after skipping two frames****** com.javatutorials.stackwalk.TheMessenger.getResponse(TheMessenger.java:13) com.javatutorials.stackwalk.WalkingTheStack.main(WalkingTheStack.java:7) ******Partial Stack with Filtered Frames by Class****** Filtered By : com.javatutorials.stackwalk.WalkingTheStack com.javatutorials.stackwalk.WalkingTheStack.main(WalkingTheStack.java:7) |
Summary
In this article, we have discussed the new Stack-Walking API introduced in Java 9. It effectively helps us to get a snapshot of the execution stack efficiently. Class references and hidden frames are excluded by default when you get an instance of StackWalker using StackWalker.getInstance(). The API was designed in this way to make sure that there is no loss in performance while traversing the execution stack. In addition to this, further enhancement in performance can be done by adding certain criterion as a lambda expression parameter to walk() method, as discussed in the last section. The StackWalker API shows better performance than getStackTrace() and SecurityManager.getClassContext().
I really like the way java 8 has made it easy to print lists. framesList.forEach(System.out::println);. Great write up. Thanks.
Yes, it does. If you have toString() for a class implemented, then it does make it easier to print.