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)用代碼的行號。
以下代碼片段顯示了如何使用Throwable
和Thread
類檢查當前線程的棧:
// 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