Tips
做一個終身學習的人。

在本章中,主要介紹以下內(nèi)容:

  • 什么是虛擬機棧(JVM Stack)和棧幀(Stack Frame)

  • 如何在JDK 9之前遍歷一個線程的棧

  • 在JDK 9中如何使用StackWalker API遍歷線程的棧

  • 在JDK 9中如何獲取調(diào)用者的類

一. 什么是虛擬機棧

JVM中的每個線程都有一個私有的JVM棧,它在創(chuàng)建線程的同時創(chuàng)建。 該棧是先進先出(LIFO)數(shù)據(jù)結(jié)構(gòu)。 棧保存棧幀。 每次調(diào)用一個方法時,都會創(chuàng)建一個新的棧幀并將其推送到棧的頂部。 當方法調(diào)用完成時,棧幀銷毀(從棧中彈出)。 堆棧中的每個棧幀都包含自己的局部變量數(shù)組,以及它自己的操作數(shù)棧,返回值和對當前方法類的運行時常量池的引用。 JVM的具體實現(xiàn)可以擴展一個棧幀來保存更多的信息。

JVM棧上的一個棧幀表示給定線程中的Java方法調(diào)用。 在給定的線程中,任何點只有一個棧幀是活動的。 活動棧幀被稱為當前棧幀,其方法稱為當前方法。 定義當前方法的類稱為當前類。 當方法調(diào)用另一種方法時,棧幀不再是當前棧幀 —— 新的棧幀被推送到棧,并且執(zhí)行方法成為當前方法,并且新棧幀成為當前棧幀。 當方法返回時,舊棧幀再次成為當前幀。 有關(guān)JVM棧和棧幀的更多詳細信息,請參閱https://docs.oracle.com/javase/specs/jvms/se8/html/index.html上的Java虛擬機規(guī)范。

Tips
如果JVM支持本地方法,則線程還包含本地方法棧,該棧包含每個本地方法調(diào)用的本地方法棧幀。

下圖顯示了兩個線程及其JVM棧。 第一個線程的JVM棧包含四個棧幀,第二個線程的JVM棧包含三個棧幀。 Frame 4是Thread-1中的活動棧幀,F(xiàn)rame 3是Thread-2中的活動棧幀。

二. 什么是虛擬機棧遍歷

虛擬機棧遍歷是遍歷線程的棧幀并檢查棧幀的內(nèi)容的過程。 從Java 1.4開始,可以獲取線程棧的快照,并獲取每個棧幀的詳細信息,例如方法調(diào)用發(fā)生的類名稱和方法名稱,源文件名,源文件中的行號等。 棧遍歷中使用的類和接口位于Stack-Walking API中。

三. JDK 8 中的棧遍歷

在JDK 9之前,可以使用java.lang包中的以下類遍歷線程棧中的所有棧幀:

  • Throwable

  • Thread

  • StackTraceElement

StackTraceElement類的實例表示棧幀。 Throwable類的getStackTrace()方法返回一含當前線程棧的棧幀的StackTraceElement []數(shù)組。 Thread類的getStackTrace()方法返回一個StackTraceElement []數(shù)組,它包含線程棧的棧幀。 數(shù)組的第一個元素是棧中的頂層棧幀,表示序列中最后一個方法調(diào)用。 JVM的一些實現(xiàn)可能會在返回的數(shù)組中省略一些棧幀。

StackTraceElement類包含以下方法,它返回由棧幀表示的方法調(diào)用的詳細信息:

String getClassLoaderName()
String getClassName()
String getFileName()
int getLineNumber()
String getMethodName()
String getModuleName()
String getModuleVersion()
boolean isNativeMethod()

Tips
在JDK 9中將getModuleName(),getModuleVersion()getClassLoaderName()方法添加到此類中。

StackTraceElement類中的大多數(shù)方法都有直觀的名稱,例如,getMethodName()方法返回調(diào)用由此棧幀表示的方法的名稱。 getFileName()方法返回包含方法調(diào)用代碼的源文件的名稱,getLineNumber()返回源文件中的方法調(diào)用代碼的行號。

以下代碼片段顯示了如何使用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());
        }
    }
}

輸出結(jié)果:

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()方法調(diào)用m1()方法,它調(diào)用m2()方法。m2()方法直接調(diào)用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規(guī)范允許虛擬機實現(xiàn)在棧中省略一些棧幀來提升性能。 因此,如果有興趣檢查整個棧,那么如果虛擬機隱藏了一些棧幀,則無法執(zhí)行此操作。

  • JDK和其他類庫中的許多API都是調(diào)用者敏感(caller-sensitive)的。 他們的行為基于調(diào)用者的類而有所不同。 例如,如果要調(diào)用Module類的addExports()方法,調(diào)用者的類必須在同一個模塊中。 否則,將拋出一個IllegalCallerException異常。 在現(xiàn)有的API中,沒有簡單而有效的方式來獲取調(diào)用者的類引用。 這樣的API依賴于使用JDK內(nèi)部API —— sun.reflect.Reflection類的getCallerClass()靜態(tài)方法。

  • 沒有簡單的方法來過濾特定實現(xiàn)類的棧幀。

五. JDK 9 中的棧遍歷

JDK 9引入了一個新的Stack-Walking API,它由java.lang包中的StackWalker類組成。 該類提供簡單而有效的棧遍歷。 它為當前線程提供了一個順序的棧幀流。 從棧生成的最上面的到最下面的棧幀,棧幀按順序記錄。 StackWalker類非常高效,因為它可以懶加載的方式地評估棧幀。 它還包含一個便捷的方法來獲取調(diào)用者類的引用。 StackWalker類由以下成員組成:

  • StackWalker.Option嵌套枚舉

  • StackWalker.StackFrame嵌套接口

  • 獲取StackWalker類實例的方法

  • 處理棧幀的方法

  • 獲取調(diào)用者類的方法

1. 指定遍歷選項

可以指定零個或多個選項來配置StackWalker。 選項是StackWalker.Option枚舉的常量。 常量如下:

  • RETAIN_CLASS_REFERENCE

  • SHOW_HIDDEN_FRAMES

  • SHOW_REFLECT_FRAMES

如果指定了RETAIN_CLASS_REFERENCE選項,則 StackWalker返回的棧幀將包含聲明由該棧幀表示的方法的類的Class對象的引用。 如果要獲取Class對象的方法調(diào)用者的引用,也需要指定此選項。 默認情況下,此選項不存在。

默認情況下,實現(xiàn)特定的和反射棧幀不包括在StackWalker類返回的棧幀中。 使用SHOW_HIDDEN_FRAMES選項來包括所有隱藏的棧幀。

如果指定了SHOW_REFLECT_FRAMES選項,則StackWalker類返回的棧幀流并包含反射棧幀。 使用此選項可能仍然隱藏實現(xiàn)特定的棧幀,可以使用SHOW_HIDDEN_FRAMES選項顯示。

2. 表示一個棧幀

在JDK 9之前,StackTraceElement類的實例被用來表示棧幀。 JDK 9中的Stack-Walker API使用StackWalker.StackFrame接口的實例來表示棧幀。

Tips
StackWalker.StackFrame接口沒有具體的實現(xiàn)類,可以直接使用。 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的結(jié)構(gòu)描述每個方法。 method_info結(jié)構(gòu)包含一個保存名為Code的可變長度屬性的屬性表。 Code屬性包含一個code的數(shù)組,它保存該方法的字節(jié)碼指令。 getByteCodeIndex()方法返回到包含由此棧幀表示的執(zhí)行點的方法的Code屬性中的代碼數(shù)組的索引。 它為本地方法返回-1。 有關(guān)代碼數(shù)組和代碼屬性的更多信息,請參閱“Java虛擬規(guī)范”第4.7.3節(jié),網(wǎng)址為https://docs.oracle.com/javase/specs/jvms/se8/html/

如何使用方法的代碼數(shù)組? 作為應用程序開發(fā)人員,不會在方法中使用字節(jié)碼索引作為執(zhí)行點。 JDK確實支持使用內(nèi)部API讀取類文件及其所有屬性。 可以使用位于JDK_HOME\bin目錄中的javap工具查看方法中每條指令的字節(jié)碼索引。 需要使用-c選項與javap打印方法的代碼數(shù)組。 以下命令顯示LegacyStackWalk類中所有方法的代碼數(shù)組:

C:\Java9Revealed>javap -c com.jdojo.stackwalker\build\classes\com\jdojo\stackwalker\LegacyStackWalk.class

輸出結(jié)果為:

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()中獲取調(diào)用棧的快照時,m2()方法調(diào)用m3()兩次。 對于第一次調(diào)用,字節(jié)碼索引為8,第二次為35。

getDeclaringClass()方法返回聲明由棧幀表示的方法的類的Class對象的引用。 如果該StackWalker沒有配置RETAIN_CLASS_REFERENCE選項,它會拋出UnsupportedOperationException異常


http://www.cnblogs.com/IcanFixIt/p/7238835.html