转载

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() 中的代码如下

try {
    throw new RuntimeException("stack");
} catch (Exception ex) {
    ex.printStackTrace();
}

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

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

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

StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
printStackTrace(pw);
s = sw.toString();

参见许多年前对 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 为我们提供了 StackWalkerStackWalker.OptionStackWalker.StackFrame

Java 9 线程栈遍历 API

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

Java 9 线程栈遍历 API

看下 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 进行过滤,映射等操作。如

List<String> list = StackWalker.getInstance(RETAIN_CLASS_REFERENCE)
    .walk(s -> s.filter(f -> !f.getDeclaringClass().getName().endsWith("Test")))
    .filter(f -> f.getMethodName().startsWith("foo"))
    .map(Object::toString).collect(toList());

由于 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://unmi.cc/java-stack-walking-api/
正文到此结束
Loading...