Java 9 揭祕(16. 虛擬機器棧遍歷)
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
例項時執行許可權檢查,而不是執行棧遍歷時。