Java 9 揭秘(16. 虚拟机栈遍历)

Tips
做一个终身学习的人。

Java 9

在本章中,主要介绍以下内容:

  • 什么是虚拟机栈(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 class
StackTraceElement[] frames = new Throwable().getStackTrace();
// Using the Thread class
StackTraceElement[] frames2 = Thread.currentThread()
                                   .getStackTrace();
// Process the frames here...

本章中的所有程序都是com.jdojo.stackwalker模块的一部分,其声明如下所示。

// module-info.java
module com.jdojo.stackwalker {
    exports com.jdojo.stackwalker;
}

下面包含一个LegacyStackWalk类的代码。 该类的输出在JDK 8中运行时生成。

// LegacyStackWalk.java
package 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异常。

toStackTraceElement()方法返回表示相堆栈帧的StackTraceElement类的实例。 如果要使用JDK 9 API来获取StackWalker.StackFrame,但是继续使用使用StackTraceElement类的旧代码来分析栈帧,这种方法非常方便。

3. 获取StackWalker

StackWalker类包含返回StackWalker实例的静态工厂方法:

StackWalker getInstance()
StackWalker getInstance (StackWalker.Option option)
StackWalker getInstance (Set<StackWalker.Option> options)
StackWalker getInstance (Set<StackWalker.Option> options, int estimateDepth)

可以使用不同版本的getInstance()方法来配置StackWalker。 默认配置是排除所有隐藏的栈帧,不保留类引用。 允许指定StackWalker.Option的版本使用这些选项进行配置。

estimateDepth参数是一个提示,指示StackWalker预计将遍历的栈帧的评估数,因此可能会优化内部缓冲区的大小。

以下代码片段创建了具有不同配置的StackWalker类的四个实例:

import java.util.Set;
import static java.lang.StackWalker.Option.*;
...
// Get a StackWalker with a default configuration
StackWalker sw1 = StackWalker.getInstance();
// Get a StackWalker that shows reflection frames
StackWalker sw2 = StackWalker.getInstance(SHOW_REFLECT_FRAMES);
// Get a StackWalker that shows all hidden frames
StackWalker sw3 = StackWalker.getInstance(SHOW_HIDDEN_FRAMES);
// Get a StackWalker that shows reflection frames and retains class references
StackWalker sw4 = StackWalker.getInstance(Set.of(SHOW_REFLECT_FRAMES, RETAIN_CLASS_REFERENCE));

Tips
StackWalker是线程安全且可重用的。 多个线程可以使用相同的实例遍历自己的栈。

4. 遍历栈

现在是遍历线程的栈帧的时候了。StackWalker类包含两个方法,可以遍历当前线程的栈:

void forEach(Consumer<? super StackWalker.StackFrame> action)
<T> T walk(Function<? super Stream<StackWalker.StackFrame>,? extends T> function)

如果需要遍历整个栈,使用forEach()方法。 指定的Consumer 将从栈中提供一个栈帧,从最上面的栈帧开始。 以下代码段打印了StackWalker返回的每个栈帧的详细信息:

// Prints the details of all stack frames of the current thread
StackWalker.getInstance()
           .forEach(System.out::println);

如果要定制栈遍历,例如使用过滤器和映射,使用walk()方法。 walk()方法接受一个Function,它接受一个Stream <StackWalker.StackFrame>作为参数,并可以返回任何类型的对象。 StackWalker将创建栈帧流并将其传递给function。 当功能完成时,StackWalker将关闭流。 传递给walk()方法的流只能遍历一次。 第二次尝试遍历流时会抛出IllegalStateException异常。

以下代码片段使用walk()方法遍历整个栈,打印每个栈帧的详细信息。 这段代码与前面的代码片段使用forEach()方法相同。

// Prints the details of all stack frames of the current thread
StackWalker.getInstance()
           .walk(s -> {
               s.forEach(System.out::println);
               return null;
            });

Tips
StackWalker的forEach()方法用于一次处理一个栈帧,而walk()方法用于处理将整个栈为帧流。 可以使用walk()方法来模拟forEach()方法的功能,但反之亦然。

可能会想知道为什么walk()方法不返回栈帧流而是将流传递给函数。 没有从方法返回堆栈帧流是有意为之的。 流的元素被懒加载的方式评估。 一旦创建了栈帧流,JVM就可以自由地重新组织栈,并且没有确定的方法来检测栈已经改变,仍然保留对其流的引用。 这就是创建和关闭栈帧流由StackWalker类控制的原因。

由于Streams API是广泛的,所以使用walk()方法。 以下代码片段获取列表中当前线程的栈帧的快照。

import java.lang.StackWalker.StackFrame;
import java.util.List;
import static java.util.stream.Collectors.toList;
...
List<StackFrame> frames = StackWalker.getInstance()
                            .walk(s -> s.collect(toList()));

以下代码段收集列表中当前线程的所有栈帧的字符串形式,不包括表示以m2开头的方法的栈帧:

mport java.util.List;
import static java.util.stream.Collectors.toList;
...
List<String> list = StackWalker.getInstance()
  .walk(s -> s.filter(f -> !f.getMethodName().startsWith("m2"))
              .map(f -> f.toString())
              .collect(toList())
       );

以下代码片段收集列表中当前线程的所有栈帧的字符串形式,不包括声明类名称以Test结尾的方法的框架:

import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import java.util.List;
import static java.util.stream.Collectors.toList;
...
List<String> list = StackWalker
    .getInstance(RETAIN_CLASS_REFERENCE)
    .walk(s -> s.filter(f -> !f.getDeclaringClass()
                               .getName().endsWith("Test"))
                .map(f -> f.toString())
                .collect(toList())
          );

以下代码段以字符串的形式收集整个栈信息,将每个栈帧与平台特定的行分隔符分隔开:

import static java.util.stream.Collectors.joining;
...
String stackStr = StackWalker.getInstance()
$.walk(s -> s.map(f -> f.toString())
             .collect(joining(System.getProperty("line.separator")
       )));

下面包含一个完整的程序,用于展示StackWalker类及其walk()方法的使用。 它的main()方法调用m1()方法两次,每次通过StackWalker的一组不同的选项。 m2()方法使用反射来调用m3()方法,它打印堆栈帧细节信息。 第一次,反射栈帧是隐藏的,类引用不可用。

// StackWalking.java
package com.jdojo.stackwalker;
import java.lang.StackWalker.Option;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import static java.lang.StackWalker.Option.SHOW_REFLECT_FRAMES;
import java.lang.StackWalker.StackFrame;
import java.lang.reflect.InvocationTargetException;
import java.util.Set;
import java.util.stream.Stream;
public class StackWalking {
    public static void main(String[] args) {
        m1(Set.of());
        System.out.println();
        // Retain class references and show reflection frames
        m1(Set.of(RETAIN_CLASS_REFERENCE, SHOW_REFLECT_FRAMES));
    }
    public static void m1(Set<Option> options) {
        m2(options);
    }
    public static void m2(Set<Option> options) {
        // Call m3() using reflection
        try {
            System.out.println("Using StackWalker Options: " + options);
            StackWalking.class
                     .getMethod("m3", Set.class)
                     .invoke(null, options);
        } catch (NoSuchMethodException
                | InvocationTargetException
                | IllegalAccessException
                | SecurityException e) {
            e.printStackTrace();
        }
    }
    public static void m3(Set<Option> options) {
        // Prints the call stack details
        StackWalker.getInstance(options)
                   .walk(StackWalking::processStack);
    }
    public static Void processStack(Stream<StackFrame> stack) {
        stack.forEach(frame -> {
            int bci = frame.getByteCodeIndex();
            String className = frame.getClassName();        
            Class<?> classRef = null;
            try {
                classRef = frame.getDeclaringClass();
            } catch (UnsupportedOperationException e) {
                // No action to take
            }
            String fileName = frame.getFileName();
            int lineNumber = frame.getLineNumber();
            String methodName = frame.getMethodName();
            boolean isNative = frame.isNativeMethod();
            StackTraceElement sfe = frame.toStackTraceElement();
            System.out.printf("Native Method=%b", isNative);
            System.out.printf(", Byte Code Index=%d", bci);
            System.out.printf(", Module Name=%s", sfe.getModuleName());
            System.out.printf(", Module Version=%s", sfe.getModuleVersion());
            System.out.printf(", Class Name=%s", className);
            System.out.printf(", Class Reference=%s", classRef);
            System.out.printf(", File Name=%s", fileName);
            System.out.printf(", Line Number=%d", lineNumber);
            System.out.printf(", Method Name=%s.%n", methodName);
        });
        return null;
    }
}

输出的结果为:

Using StackWalker Options: []
Native Method=false, Byte Code Index=9, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=null, FileName=StackWalking.java, Line Number=44, Method Name=m3.
Native Method=false, Byte Code Index=37, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=null, File Name=StackWalking.java, Line Number=32, Method Name=m2.
Native Method=false, Byte Code Index=1, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=null, File Name=StackWalking.java, Line Number=23, Method Name=m1.
Native Method=false, Byte Code Index=3, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=null, File Name=StackWalking.java, Line Number=14, Method Name=main .
Using StackWalker Options: [SHOW_REFLECT_FRAMES, RETAIN_CLASS_REFERENCE]
Native Method=false, Byte Code Index=9, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=class com.jdojo.stackwalker.StackWalking, File Name=StackWalking.java, Line Number=44, Method Name=m3.
Native Method=true, Byte Code Index=-1, Module Name=java.base, Module Version=9-ea, Class Name=jdk.internal.reflect.NativeMethodAccessorImpl, Class Reference=class jdk.internal.reflect.NativeMethodAccessorImpl, File Name=NativeMethodAccessorImpl.java, Line Number=-2, Method Name=invoke0.
Native Method=false, Byte Code Index=100, Module Name=java.base, Module Version=9-ea, Class Name=jdk.internal.reflect.NativeMethodAccessorImpl, Class Reference=class jdk.internal.reflect.NativeMethodAccessorImpl, File Name=NativeMethodAccessorImpl.java, Line Number=62, Method Name=invoke.
Native Method=false, Byte Code Index=6, Module Name=java.base, Module Version=9-ea, Class Name=jdk.internal.reflect.DelegatingMethodAccessorImpl, Class Reference=class jdk.internal.reflect.DelegatingMethodAccessorImpl, File Name=DelegatingMethodAccessorImpl.java, Line Number=43, Method Name=invoke.
Native Method=false, Byte Code Index=59, Module Name=java.base, Module Version=9-ea, Class Name=java.lang.reflect.Method, Class Reference=class java.lang.reflect.Method, File Name=Method.java, Line Number=538, Method Name=invoke.
Native Method=false, Byte Code Index=37, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=class com.jdojo.stackwalker.StackWalking, File Name=StackWalking.java, Line Number=32, Method Name=m2.
Native Method=false, Byte Code Index=1, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=class com.jdojo.stackwalker.StackWalking, File Name=StackWalking.java, Line Number=23, Method Name=m1.
Native Method=false, Byte Code Index=21, Module Name=null, Module Version=null, Class Name=com.jdojo.stackwalker.StackWalking, Class Reference=class com.jdojo.stackwalker.StackWalking, File Name=StackWalking.java, Line Number=19, Method Name=main .

5. 认识调用者的类

在JDK 9之前,开发人员依靠以下方法来获取调用者的调用:

  • SecurityManager类的getClassContext()方法,由于该方法受到保护,因此需要进行子类化。
  • sun.reflect.Reflection类的getCallerClass()方法,它是一个JDK内部类。

JDK 9通过在StackWalker类中添加一个getCallerClass()的方法,使得获取调用者类引用变得容易。 方法的返回类型是Class<?>。 如果StackWalker未配置RETAIN_CLASS_REFERENCE选项,则调用此方法将抛出UnsupportedOperationException异常。 如果栈中没有调用者栈帧,则调用此方法会引发IllegalStateException,例如,运行main()方法调用此方法的类。

那么,哪个类是调用类? 在Java中,方法和构造函数可调用。 以下讨论使用方法,但是它也适用于构造函数。 假设在S的方法中调用getCallerClass()方法,该方法从T的方法调用。另外假设T的方法在名为C的类中。在这种情况下,C类是调用者类。

Tips
StackWalker类的getCallerClass()方法在查找调用者类时会过滤所有隐藏和反射栈帧,而不管用于获取StackWalker实例的选项如何。

下面包含一个完整的程序来显示如何获取调用者的类。 它的main()方法调用m1()方法,m1调用m2()方法,m2调用m3()方法。 m3()方法获取StackWalker类的实例并获取调用者类。 请注意,m2()方法使用反射来调用m3()方法。 最后,main()方法尝试获取调用者类。 当运行CallerClassTest类时,main()方法由JVM调用,栈上不会有调用者栈帧。 这将抛出一个IllegalStateException异常。

// CallerClassTest.java
package com.jdojo.stackwalker;
import java.lang.StackWalker.Option;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
import static java.lang.StackWalker.Option.SHOW_REFLECT_FRAMES;
import java.lang.reflect.InvocationTargetException;
import java.util.Set;
public class CallerClassTest {
    public static void main(String[] args) {
        /* Will not be able to get caller class because because the RETAIN_CLASS_REFERENCE
           option is not specified.
        */
        m1(Set.of());
        // Will print the caller class
        m1(Set.of(RETAIN_CLASS_REFERENCE, SHOW_REFLECT_FRAMES));
        try {
            /* The following statement will throw an IllegalStateException if this class is run
               because there will be no caller class; JVM will call this method. However,
               if the main() method is called in code, no exception will be thrown.            
            */
            Class<?> cls = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
                                      .getCallerClass();
            System.out.println("In main method, Caller Class: " + cls.getName());
        } catch (IllegalCallerException e) {
            System.out.println("In main method, Exception: " + e.getMessage());
        }
    }
    public static void m1(Set<Option> options) {
        m2(options);
    }
    public static void m2(Set<Option> options) {
        // Call m3() using reflection
        try {
            CallerClassTest.class
                           .getMethod("m3", Set.class)
                           .invoke(null, options);
        } catch (NoSuchMethodException | InvocationTargetException
                | IllegalAccessException | SecurityException e) {
            e.printStackTrace();
        }
    }
    public static void m3(Set<Option> options) {
        try {
            // Print the caller class
            Class<?> cls = StackWalker.getInstance(options)                  
                                      .getCallerClass();
            System.out.println("Caller Class: " + cls.getName());
        } catch (UnsupportedOperationException e) {
            System.out.println("Inside m3(): " + e.getMessage());
        }
    }
}

输出结果为:

Inside m3(): This stack walker does not have RETAIN_CLASS_REFERENCE access
Caller Class: com.jdojo.stackwalker.CallerClassTest
In main method, Exception: no caller frame

在前面的例子中,收集栈帧的方法是从同一个类的另一个方法中调用的。 我们从另一个类的方法中调用这个方法来看到一个不同的结果。 下面显示了CallerClassTest2的类的代码。

// CallerClassTest2.java
package com.jdojo.stackwalker;
import java.lang.StackWalker.Option;
import java.util.Set;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
public class CallerClassTest2 {
    public static void main(String[] args) {
        Set<Option> options = Set.of(RETAIN_CLASS_REFERENCE);
        CallerClassTest.m1(options);
        CallerClassTest.m2(options);
        CallerClassTest.m3(options);
        System.out.println("\nCalling the main() method:");
        CallerClassTest.main(null);
        System.out.println("\nUsing an anonymous class:");
        new Object() {
            {
                CallerClassTest.m3(options);
            }   
        };
        System.out.println("\nUsing a lambda expression:");
        new Thread(() -> CallerClassTest.m3(options))
            .start();
    }
}

输出结果为:

Caller Class: com.jdojo.stackwalker.CallerClassTest
Caller Class: com.jdojo.stackwalker.CallerClassTest
Caller Class: com.jdojo.stackwalker.CallerClassTest2
Calling the main() method:
Inside m3(): This stack walker does not have RETAIN_CLASS_REFERENCE access
Caller Class: com.jdojo.stackwalker.CallerClassTest
In main method, Caller Class: com.jdojo.stackwalker.CallerClassTest2
Using an anonymous class:
Caller Class: com.jdojo.stackwalker.CallerClassTest2$1
Using a lambda expression:
Caller Class: com.jdojo.stackwalker.CallerClassTest2

CallerClassTest2类的main()方法调用CallerClassTest类的四个方法。 当CallerClassTest.m3()CallerClassTest2类直接调用时,调用者类是CallerClassTest2。 当从CallerClassTest2类调用CallerClassTest.main()方法时,有一个调用者栈帧,调用者类是CallerClassTest2类。 当运行CallerClassTest类时,将其与上一个示例的输出进行比较。 那时,CallerClassTest.main()方法是从JVM调用的,不能在CallerClassTest.main()方法中获得一个调用者类,因为没有调用者栈帧。 最后,CallerClassTest.m3()方法从匿名类和lambda表达式调用。 匿名类被报告为调用者类。 在lambda表达式的情况下,它的闭合类被报告为调用者类。

6. 栈遍历权限

当存在Java安全管理器并且使用RETAIN_CLASS_REFERENCE选项配置StackWalker时,将执行权限检查,以确保代码库被授予retainClassReferencejava.lang.StackFramePermission值。 如果未授予权限,则抛出SecurityException异常。 在创建StackWalker实例时执行权限检查,而不是在执行栈遍历时。

下包含StackWalkerPermissionCheck类的代码。 它的printStackFrames()方法使用RETAIN_CLASS_REFERENCE选项创建StackWalker实例。 假设没有安全管理器,main()方法调用此方法,它打印堆栈跟踪没有任何问题。 安装安全管理器以后,再次调用printStackFrames()方法。 这一次,抛出一个SecurityException异常,这在输出中显示。

// StackWalkerPermissionCheck.java
package com.jdojo.stackwalker;
import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
public class StackWalkerPermissionCheck {
    public static void main(String[] args) {
        System.out.println("Before installing security manager:");
        printStackFrames();
        SecurityManager sm = System.getSecurityManager();
        if (sm == null) {
            sm = new SecurityManager();
            System.setSecurityManager(sm);
        }
        System.out.println(
            "\nAfter installing security manager:");
        printStackFrames();
    }
    public static void printStackFrames() {
        try {
            StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
                       .forEach(System.out::println);
        } catch(SecurityException  e){
            System.out.println("Could not create a " +
                "StackWalker. Error: " + e.getMessage());
        }
    }
}

输出结果为:

Before installing security manager:
com.jdojo.stackwalker/com.jdojo.stackwalker.StackWalkerPermissionCheck.printStackFrames(StackWalkerPermissionCheck.java:24)
com.jdojo.stackwalker/com.jdojo.stackwalker.StackWalkerPermissionCheck.main(StackWalkerPermissionCheck.java:9)
After installing security manager:
Could not create a StackWalker. Error: access denied ("java.lang.StackFramePermission" "retainClassReference")

下面显示了如何使用RETAIN_CLASS_REFERENCE选项授予创建StackWalker所需的权限。 授予所有代码库的权限,需要将此权限块添加到位于机器上的JAVA_HOME\conf\security目录中的java.policy文件的末尾。

grant {
    permission java.lang.StackFramePermission "retainClassReference";
};

当授予权限以后再运行上面的类时,应该会收到以下输出:

Before installing security manager:
com.jdojo.stackwalker/com.jdojo.stackwalker.StackWalkerPermissionCheck.printStackFrames(StackWalkerPermissionCheck.java:24)
com.jdojo.stackwalker/com.jdojo.stackwalker.StackWalkerPermissionCheck.main(StackWalkerPermissionCheck.java:9)
After installing security manager:
com.jdojo.stackwalker/com.jdojo.stackwalker.StackWalkerPermissionCheck.printStackFrames(StackWalkerPermissionCheck.java:24)
com.jdojo.stackwalker/com.jdojo.stackwalker.StackWalkerPermissionCheck.main(StackWalkerPermissionCheck.java:18)

六. 总结

JVM中的每个线程都有一个私有的JVM栈,它在创建线程的同时创建。 栈保存栈帧。 JVM栈上的一个栈帧表示给定线程中的Java方法调用。 每次调用一个方法时,都会创建一个新的栈帧并将其推送到栈的顶部。 当方法调用完成时,框架被销毁(从堆栈中弹出)。 在给定的线程中,任何点只有一个栈帧是活动的。 活动栈帧被称为当前栈帧,其方法称为当前方法。 定义当前方法的类称为当前类。

在JDK 9之前,可以使用以下类遍历线程栈中的所有栈帧:ThrowablehreadStackTraceElementStackTraceElement类的实例表示栈帧。 Throwable类的getStrackTrace()方法返回包含当前线程栈帧的StackTraceElement []Thread类的getStrackTrace()方法返回包含线程栈帧的StackTraceElement []。 数组的第一个元素是栈中的顶层栈帧,表示序列中最后一个方法调用。 一些JVM的实现可能会在返回的数组中省略一些栈帧。

JDK 9使栈遍历变得容易。 它在java.lang包中引入了一个StackWalker的新类。 可以使用getInstance()的静态工厂方法获取StackWalker的实例。 可以使用StackWalker.Option的枚举中定义的常量来表示的选项来配置StackWalkerStackWalker.StackFrame的嵌套接口的实例表示栈帧。 StackWalker类与StackWalker.StackFrame实例配合使用。 该接口定义了toStackTraceElement()的方法,可用于从StackWalker.StackFrame获取StackTraceElement类的实例。

可以使用StackWalker实例的forEach()walk()方法遍历当前线程的栈帧。 StackWalker实例的getCallerClass()方法返回调用者类引用。 如果想要代表栈帧的类的引用和调用者类的引用,则必须使用RETAIN_CLASS_REFERENCE配置StackWalker实例。 默认情况下,所有反射栈帧和实现特定的栈帧都不会被StackWalker记录。 如果希望这些框架包含在栈遍历中,请使用SHOW_REFLECT_FRAMES和SHOW_HIDDEN_FRAMES选项来配置StackWalker。 使用SHOW_HIDDEN_FRAMES选项也包括反栈帧。

当存在Java安全管理器并且使用RETAIN_CLASS_REFERENCE选项配置StackWalker时,将执行权限检查,以确保代码库被授予retainClassReferencejava.lang.StackFramePermission值。 如果未授予权限,则抛出SecurityException异常。 在创建StackWalker实例时执行权限检查,而不是执行栈遍历时。

posted @ 2017-07-26 11:49  林本托  阅读(2174)  评论(2编辑  收藏  举报