Tips
做一个终身学习的人。
在本章中,主要介绍以下内容:
什么是虚拟机栈(JVM Stack)和栈帧(Stack Frame)
如何在JDK 9之前遍历一个线程的栈
在JDK 9中如何使用StackWalker API遍历线程的栈
在JDK 9中如何获取调用者的类
一. 什么是虚拟机栈
JVM中的每个线程都有一个私有的JVM栈,它在创建线程的同时创建。 该栈是先进先出(LIFO)数据结构。 栈保存栈帧。 每次调用一个方法时,都会创建一个新的栈帧并将其推送到栈的顶部。 当方法调用完成时,栈帧销毁(从栈中弹出)。 堆栈中的每个栈帧都包含自己的局部变量数组,以及它自己的操作数栈,返回值和对当前方法类的运行时常量池的引用。 JVM的具体实现可以扩展一个栈帧来保存更多的信息。
JVM栈上的一个栈帧表示给定线程中的Java方法调用。 在给定的线程中,任何点只有一个栈帧是活动的。 活动栈帧被称为当前栈帧,其方法称为当前方法。 定义当前方法的类称为当前类。 当方法调用另一种方法时,栈帧不再是当前栈帧 —— 新的栈帧被推送到栈,并且执行方法成为当前方法,并且新栈帧成为当前栈帧。 当方法返回时,旧栈帧再次成为当前帧。 有关JVM栈和栈帧的更多详细信息,请参阅https://docs.oracle.com/javase/specs/jvms/se8/html/index.html上的Java虚拟机规范。
Tips
如果JVM支持本地方法,则线程还包含本地方法栈,该栈包含每个本地方法调用的本地方法栈帧。
下图显示了两个线程及其JVM栈。 第一个线程的JVM栈包含四个栈帧,第二个线程的JVM栈包含三个栈帧。 Frame 4是Thread-1中的活动栈帧,Frame 3是Thread-2中的活动栈帧。
二. 什么是虚拟机栈遍历
虚拟机栈遍历是遍历线程的栈帧并检查栈帧的内容的过程。 从Java 1.4开始,可以获取线程栈的快照,并获取每个栈帧的详细信息,例如方法调用发生的类名称和方法名称,源文件名,源文件中的行号等。 栈遍历中使用的类和接口位于Stack-Walking API中。
三. JDK 8 中的栈遍历
在JDK 9之前,可以使用java.lang包中的以下类遍历线程栈中的所有栈帧:
Throwable
Thread
StackTraceElement
StackTraceElement
类的实例表示栈帧。 Throwable
类的getStackTrace()
方法返回一含当前线程栈的栈帧的StackTraceElement []
数组。 Thread
类的getStackTrace()
方法返回一个StackTraceElement []
数组,它包含线程栈的栈帧。 数组的第一个元素是栈中的顶层栈帧,表示序列中最后一个方法调用。 JVM的一些实现可能会在返回的数组中省略一些栈帧。
StackTraceElement
类包含以下方法,它返回由栈帧表示的方法调用的详细信息:
String getClassLoaderName() String getClassName() String getFileName() int getLineNumber() String getMethodName() String getModuleName() String getModuleVersion() boolean isNativeMethod()
Tips
在JDK 9中将getModuleName()
,getModuleVersion()
和getClassLoaderName()
方法添加到此类中。
StackTraceElement
类中的大多数方法都有直观的名称,例如,getMethodName()
方法返回调用由此栈帧表示的方法的名称。 getFileName()
方法返回包含方法调用代码的源文件的名称,getLineNumber()
返回源文件中的方法调用代码的行号。
以下代码片段显示了如何使用Throwable
和Thread
类检查当前线程的栈:
// Using the Throwable classStackTraceElement[] frames = new Throwable().getStackTrace();// Using the Thread classStackTraceElement[] frames2 = Thread.currentThread() .getStackTrace();// Process the frames here...
本章中的所有程序都是com.jdojo.stackwalker模块的一部分,其声明如下所示。
// module-info.javamodule com.jdojo.stackwalker { exports com.jdojo.stackwalker; }
下面包含一个LegacyStackWalk
类的代码。 该类的输出在JDK 8中运行时生成。
// LegacyStackWalk.javapackage com.jdojo.stackwalker; import java.lang.reflect.InvocationTargetException;public class LegacyStackWalk { public static void main(String[] args) { m1(); } public static void m1() { m2(); } public static void m2() { // Call m3() directly System.out.println("\nWithout using reflection: "); m3(); // Call m3() using reflection try { System.out.println("\nUsing reflection: "); LegacyStackWalk.class .getMethod("m3") .invoke(null); } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | SecurityException e) { e.printStackTrace(); } } public static void m3() { // Prints the call stack details StackTraceElement[] frames = Thread.currentThread() .getStackTrace(); for(StackTraceElement frame : frames) { System.out.println(frame.toString()); } } }
输出结果:
java.lang.Thread.getStackTrace(Thread.java:1552)com.jdojo.stackwalker.LegacyStackWalk.m3(LegacyStackWalk.java:37)com.jdojo.stackwalker.LegacyStackWalk.m2(LegacyStackWalk.java:18)com.jdojo.stackwalker.LegacyStackWalk.m1(LegacyStackWalk.java:12)com.jdojo.stackwalker.LegacyStackWalk.main(LegacyStackWalk.java:8)Using reflection:java.lang.Thread.getStackTrace(Thread.java:1552)com.jdojo.stackwalker.LegacyStackWalk.m3(LegacyStackWalk.java:37)sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)java.lang.reflect.Method.invoke(Method.java:498)com.jdojo.stackwalker.LegacyStackWalk.m2(LegacyStackWalk.java:25)com.jdojo.stackwalker.LegacyStackWalk.m1(LegacyStackWalk.java:12)com.jdojo.stackwalker.LegacyStackWalk.main(LegacyStackWalk.java:8)
LegacyStackWalk
类的main()
方法调用m1()
方法,它调用m2()
方法。m2()
方法直接调用m3()
方法两次,其中一次使用了反射。 m3()
方法使用Thread
类的getStrackTrace()
方法获取当前线程栈快照,并使用StackTraceElement
类的toString()
方法打印栈帧的详细信息。 可以使用此类的方法来获取每个栈帧的相同信息。 当在JDK 9中运行LegacyStackWalk
类时,输出包括每行开始处的模块名称和模块版本。 JDK 9的输出如下:
Without using reflection:java.base/java.lang.Thread.getStackTrace(Thread.java:1654) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.m3(LegacyStackWalk.java:37) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.m2(LegacyStackWalk.java:18) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.m1(LegacyStackWalk.java:12) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.main(LegacyStackWalk.java:8) Using reflection:java.base/java.lang.Thread.getStackTrace(Thread.java:1654) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.m3(LegacyStackWalk.java:37) 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:538) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.m2(LegacyStackWalk.java:25) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.m1(LegacyStackWalk.java:12) com.jdojo.stackwalker/com.jdojo.stackwalker.LegacyStackWalk.main(LegacyStackWalk.java:8)
四. JDK 8 的栈遍历的缺点
在JDK 9之前,Stack-Walking API存在以下缺点:
效率不高。
Throwable
类的getStrackTrace()
方法返回整个栈的快照。 没有办法在栈中只得到几个顶部栈帧。栈帧包含方法名称和类名称,而不是类引用。 类引用是
Class<?>
类的实例,而类名只是字符串。JVM规范允许虚拟机实现在栈中省略一些栈帧来提升性能。 因此,如果有兴趣检查整个栈,那么如果虚拟机隐藏了一些栈帧,则无法执行此操作。
JDK和其他类库中的许多API都是调用者敏感(caller-sensitive)的。 他们的行为基于调用者的类而有所不同。 例如,如果要调用
Module
类的addExports()
方法,调用者的类必须在同一个模块中。 否则,将抛出一个IllegalCallerException
异常。 在现有的API中,没有简单而有效的方式来获取调用者的类引用。 这样的API依赖于使用JDK内部API ——sun.reflect.Reflection
类的getCallerClass()
静态方法。没有简单的方法来过滤特定实现类的栈帧。
五. JDK 9 中的栈遍历
JDK 9引入了一个新的Stack-Walking API,它由java.lang包中的StackWalker
类组成。 该类提供简单而有效的栈遍历。 它为当前线程提供了一个顺序的栈帧流。 从栈生成的最上面的到最下面的栈帧,栈帧按顺序记录。 StackWalker
类非常高效,因为它可以懒加载的方式地评估栈帧。 它还包含一个便捷的方法来获取调用者类的引用。 StackWalker
类由以下成员组成:
StackWalker.Option
嵌套枚举StackWalker.StackFrame
嵌套接口获取
StackWalker
类实例的方法处理栈帧的方法
获取调用者类的方法
1. 指定遍历选项
可以指定零个或多个选项来配置StackWalker
。 选项是StackWalker.Option
枚举的常量。 常量如下:
RETAIN_CLASS_REFERENCE
SHOW_HIDDEN_FRAMES
SHOW_REFLECT_FRAMES
如果指定了RETAIN_CLASS_REFERENCE选项,则 StackWalker
返回的栈帧将包含声明由该栈帧表示的方法的类的Class
对象的引用。 如果要获取Class
对象的方法调用者的引用,也需要指定此选项。 默认情况下,此选项不存在。
默认情况下,实现特定的和反射栈帧不包括在StackWalker
类返回的栈帧中。 使用SHOW_HIDDEN_FRAMES选项来包括所有隐藏的栈帧。
如果指定了SHOW_REFLECT_FRAMES选项,则StackWalker
类返回的栈帧流并包含反射栈帧。 使用此选项可能仍然隐藏实现特定的栈帧,可以使用SHOW_HIDDEN_FRAMES选项显示。
2. 表示一个栈帧
在JDK 9之前,StackTraceElement
类的实例被用来表示栈帧。 JDK 9中的Stack-Walker API使用StackWalker.StackFrame
接口的实例来表示栈帧。
Tips
StackWalker.StackFrame
接口没有具体的实现类,可以直接使用。 JDK中的Stack-Walking API在检索栈帧时为你提供了接口的实例。
StackWalker.StackFrame
接口包含以下方法,其中大部分与StackTraceElement
类中的方法相同:
int getByteCodeIndex()String getClassName()Class<?> getDeclaringClass()String getFileName() int getLineNumber()String getMethodName()boolean isNativeMethod() StackTraceElement toStackTraceElement()
在类文件中,使用为method_info的结构描述每个方法。 method_info结构包含一个保存名为Code的可变长度属性的属性表。 Code属性包含一个code的数组,它保存该方法的字节码指令。 getByteCodeIndex()
方法返回到包含由此栈帧表示的执行点的方法的Code属性中的代码数组的索引。 它为本地方法返回-1。 有关代码数组和代码属性的更多信息,请参阅“Java虚拟规范”第4.7.3节,网址为https://docs.oracle.com/javase/specs/jvms/se8/html/。
如何使用方法的代码数组? 作为应用程序开发人员,不会在方法中使用字节码索引作为执行点。 JDK确实支持使用内部API读取类文件及其所有属性。 可以使用位于JDK_HOME\bin目录中的javap工具查看方法中每条指令的字节码索引。 需要使用-c
选项与javap打印方法的代码数组。 以下命令显示LegacyStackWalk
类中所有方法的代码数组:
C:\Java9Revealed>javap -c com.jdojo.stackwalker\build\classes\com\jdojo\stackwalker\LegacyStackWalk.class
输出结果为:
Compiled from "LegacyStackWalk.java"public class com.jdojo.stackwalker.LegacyStackWalk { public com.jdojo.stackwalker.LegacyStackWalk(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: invokestatic #2 // Method m1:()V 3: return public static void m1(); Code: 0: invokestatic #3 // Method m2:()V 3: return public static void m2(); Code: 0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String \nWithout using reflection: 5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: invokestatic #7 // Method m3:()V... 32: anewarray #13 // class java/lang/Object 35: invokevirtual #14 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;... public static void m3(); Code: 0: invokestatic #20 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread; 3: invokevirtual #21 // Method java/lang/Thread.getStackTrace:()[Ljava/lang/StackTraceElement;... }
当在方法m3()
中获取调用栈的快照时,m2()
方法调用m3()
两次。 对于第一次调用,字节码索引为8,第二次为35。
getDeclaringClass()
方法返回声明由栈帧表示的方法的类的Class
对象的引用。 如果该StackWalker
没有配置RETAIN_CLASS_REFERENCE选项,它会抛出UnsupportedOperationException
异常
http://www.cnblogs.com/IcanFixIt/p/7238835.html