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實例時執行權限檢查,而不是執行棧遍歷時。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM