Tips
做一個終身學習的人。
在本章中,主要介紹以下內容:
- 什么是虛擬機棧(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()
返回源文件中的方法調用代碼的行號。
以下代碼片段顯示了如何使用Throwable
和Thread
類檢查當前線程的棧:
// 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
StackWalke
r的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
時,將執行權限檢查,以確保代碼庫被授予retainClassReference
的java.lang.StackFramePermission
值。 如果未授予權限,則拋出SecurityException
異常。 在創建StackWalke
r實例時執行權限檢查,而不是在執行棧遍歷時。
下包含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之前,可以使用以下類遍歷線程棧中的所有棧幀:Throwable
,hread
和StackTraceElement
。 StackTraceElement
類的實例表示棧幀。 Throwable
類的getStrackTrace()
方法返回包含當前線程棧幀的StackTraceElement []
。 Thread
類的getStrackTrace()
方法返回包含線程棧幀的StackTraceElement []
。 數組的第一個元素是棧中的頂層棧幀,表示序列中最后一個方法調用。 一些JVM的實現可能會在返回的數組中省略一些棧幀。
JDK 9使棧遍歷變得容易。 它在java.lang包中引入了一個StackWalker
的新類。 可以使用getInstance()
的靜態工廠方法獲取StackWalker
的實例。 可以使用StackWalker.Option
的枚舉中定義的常量來表示的選項來配置StackWalker
。 StackWalker.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
時,將執行權限檢查,以確保代碼庫被授予retainClassReference
的java.lang.StackFramePermission
值。 如果未授予權限,則拋出SecurityException
異常。 在創建StackWalker
實例時執行權限檢查,而不是執行棧遍歷時。