1. 程式人生 > >Java 9 揭祕(16. 虛擬機器棧遍歷)

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