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()返回源文件中的方法调用代码的行号。

以下代码片段显示了如何使用ThrowableThread类检查当前线程的栈:

// 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

延伸阅读

向着Java前进-Java培训,做最负责任的教育,学习改变命运,软件学习,再就业,大学生如何就业,帮大学生找到好工作,lphotoshop培训,电脑培训,电脑维修培训,移动软件开发培训,网站设计培训,网站建设培训向着Java前进