什么是线程栈
继续纠缠 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() 中的代码如下
1 2 3 4 5 |
try { throw new RuntimeException("stack"); } catch (Exception ex) { ex.printStackTrace(); } |
上面输出的每一行就是一个栈桢,输出了当前类名,方法名,代码行号。
Java 9 之前如何获得线程栈信息
我们这儿要说的线程栈就是这个东西,先不交代 Java 9 遍历它的新 API,那么在 Java 9 之前要如何得到如上的信息呢?其实前面就是一个例子,printStackTrace()
是出自于 Throwable
的方法,上面是输出到了控制台,Log4J 1.2.13 只是把栈信息保存到了字符串了
1 2 3 4 |
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 为我们提供了 StackWalker
,StackWalker.Option
和 StackWalker.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
进行过滤,映射等操作。如
1 2 3 4 |
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://yanbin.blog/java-stack-walking-api/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。