Java 9 线程栈遍历 API

什么是线程栈

继续纠缠 Java 9 的新特性,仍然是一个边角料,即 Java 9 增加了对线程栈遍历的 API。那么什么是线程栈,JVM 在创建每一个线程的同时都会创建一个私有的虚拟机栈,每一桢代表着一个方法调用,每次方法的调用与退出意味着压栈与出栈。每一桢上有局部变量,操作数常量引用等信息,这也是为什么局部变量是能最快被销毁的对象。过深的栈(比如过多的递归调用) 会出现我们程序员赖以生存的 StackOverflow。

浅显些说,线程栈就是通常我们捕获到异常后,用 e.printStackTrace() 看到自 main 方法追溯到当前方法的调用。例如:

java.lang.RuntimeException: stack
    at cc.unmi.TestStackWalking.m2(TestStackWalking.java:15)
    at cc.unmi.TestStackWalking.m1(TestStackWalking.java:10)
    at cc.unmi.TestStackWalking.main(TestStackWalking.java:6)

调用层次是 main() 调用 m1(), m1() 调用 m2(), m2() 中的代码如下

上面输出的每一行就是一个栈桢,输出了当前类名,方法名,代码行号。

Java 9 之前如何获得线程栈信息

我们这儿要说的线程栈就是这个东西,先不交代 Java 9 遍历它的新 API,那么在 Java 9 之前要如何得到如上的信息呢?其实前面就是一个例子,printStackTrace() 是出自于 Throwable 的方法,上面是输出到了控制台,Log4J 1.2.13 只是把栈信息保存到了字符串了

参见许多年前对 Log4J 如何定位代码信息的研究 Log4J 输出日志时是如何获知当前方法、行号的,Log4J 1.2.13 后的代码实现可能略有不同。

实质上,在 Java 9 之前有两种方法来获得线程栈信息

Throwable.getStackTrace():   StackTraceElement[]   @Since 1.4
Thread.getStackTrace(): StackTraceElement[]   @Since 1.5

StackTraceElement[] 就是自顶向下的线程栈,我们能获得的每一桢的信息就是 StackTraceElement,它能给予我们的是

getClassName(): String
getFileName(): String
getLineNumber(): int
getMethodName(): String
isNativeMethod(): boolean

当了,到了 Java 9 之后还外加两个模块相关的信息和类加载器名

getModuleName(): String
getModuleVersion(): String
getClassLoaderName(): String

getStackTrace() 的几个弊端:注意到从 StackTraceElement 中不能直接拿到类引用(Class<?>), 或者可以用当前线程加载器来加载 getClassName() 来获得类引用。getStackTrace() 总是返回整个线程栈的快照,即使是只关注上面几桢。为性能考虑,某些桢可能被 JVM 实现隐藏。

Java 9 如何获得线程栈信息

Java 9 为我们提供了 StackWalker ,StackWalker.OptionStackWalker.StackFrame 类

StackWalker 有四个工厂方法 getInstance(...), 再通过 StackWalker 的 forEach(...) 或 walk(...) 来遍历其中的 StackFrame

看下 StackFrame 有什么内容,

getByteCodeIndex(): int
getClassName(): String
getDeclaringClass(): Class<?>
getFileName(): String
getLineNumber(): int
getMethodName(): String
isNativeMethod(): boolean
toStackTraceElement(): StackTraceElement

StackTrackElement 有很多相同的东西,多的是 getByteCodeIndex() 和 getDeclaringClass(), 前者一般不太关心,后者有时候还是有用的。看来想要获得模块名和版本还是调用 toStackTraceElement() 才行。

StackWalker.getInstance(...) 接收的几个 StackWalker.Option

RETAIN_CLASS_REFERENCE:  遍历时调用 getDeclaringClass() 需要指名该选项,否则出现 UnsupportedOperationException
SHOW_HIDDEN_FRAMES: 显示所有的隐藏桢
SHOW_REFLECT_FRAMES: 当用反射方式调用时把反射过程的方法调用桢也显示,通过反射来调用方法的话需留意它。可能它要与 SHOW_HIDDEN_FRAMES 一同使用。Java 9 之前的 getStackTrace(): StackTraceElement[] 返回的调用栈总是包含反射方法桢,这一点 Java 9 就聪明一些。

关于 StackWalker 的两个遍历方法,forEach(...) 没什么说的,walk(..) 方法让我们让进 StackFrame 进行过滤,映射等操作。如

由于 walk(...) 方法操作的是一个 Stream,因此它有管道和延迟评估的特性

想知道方法的调用者是谁

从前面的 StackWalker API 中看到有一个方法是 getCallerClass(), Java 9 想要知道谁是调用者就这么简单,记得在获得 StackWalker 实例时需指定 RETAIN_CLASS_REFERENCE, 否则也是 UnsupportedOperationException。如果已是 main 方法,没有 caller 是,就会报出 IllegalStateException 异常。

借助于 Caller's Class, 我们可基本调用关系(control flow) 来控制实现逻辑,比如 A 调用我干这事,B 调用我的话就干那事。

类似的,在 Java 9 之前想要知道谁是调用者,可以在 StackTraceElement[] 中往前推,或用 JDK 内部方法 sun.reflect.Reflection.getCallerClass(), 或者调用 SecurityManager.getClassContext(): Class<?>[], 这是一个本地方法,它是受保护的,想调用还得创建 SecurityManager 的子类,未曾尝试过。

注意:StackWalker.getCallerClass() 总是会跳过隐藏的和反射调用桢,不管你的 StackWalker.Option 指定的是什么。


随着 Java 9 的 StackWalker API 的加入,也许以后的日志框架,Log4J, Logback 等能用上这些新的 API 来输出日志所在代码位置信息,在一定程度上兴许对性能有点改善。

本文链接 https://yanbin.blog/java-stack-walking-api/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments