什么是线程栈
继续纠缠 Java 9 的新特性,仍然是一个边角料,即 Java 9 增加了对线程栈遍历的 API。那么什么是线程栈,JVM 在创建每一个线程的同时都会创建一个私有的虚拟机栈,每一桢代表着一个方法调用,每次方法的调用与退出意味着压栈与出栈。每一桢上有局部变量,操作数常量引用等信息,这也是为什么局部变量是能最快被销毁的对象。过深的栈(比如过多的递归调用) 会出现我们程序员赖以生存的 StackOverflow。
浅显些说,线程栈就是通常我们捕获到异常后,用 e.printStackTrace() 看到自 main 方法追溯到当前方法的调用。例如:
java.lang.RuntimeException:stackatcc.unmi.TestStackWalking.m2(TestStackWalking.java:15)atcc.unmi.TestStackWalking.m1(TestStackWalking.java:10)atcc.unmi.TestStackWalking.main(TestStackWalking.java:6)
调用层次是 main() 调用 m1(), m1() 调用 m2(), m2() 中的代码如下
try{thrownewRuntimeException("stack");}catch(Exceptionex) { ex.printStackTrace();}
上面输出的每一行就是一个栈桢,输出了当前类名,方法名,代码行号。
Java 9 之前如何获得线程栈信息
我们这儿要说的线程栈就是这个东西,先不交代 Java 9 遍历它的新 API,那么在 Java 9 之前要如何得到如上的信息呢?其实前面就是一个例子, printStackTrace() 是出自于 Throwable的方法,上面是输出到了控制台,Log4J 1.2.13 只是把栈信息保存到了字符串了
StringWriter sw =newStringWriter();PrintWriter pw =newPrintWriter(sw);printStackTrace(pw);s = sw.toString();
参见许多年前对 Log4J 如何定位代码信息的研究 Log4J 输出日志时是如何获知当前方法、行号的 ,Log4J 1.2.13 后的代码实现可能略有不同。
实质上,在 Java 9 之前有两种方法来获得线程栈信息
Throwable.getStackTrace():StackTraceElement[]@Since1.4Thread.getStackTrace():StackTraceElement[]@Since1.5
StackTraceElement[] 就是自顶向下的线程栈,我们能获得的每一桢的信息就是 StackTraceElement,它能给予我们的是
getClassName():StringgetFileName():StringgetLineNumber():intgetMethodName():StringisNativeMethod():boolean
当了,到了 Java 9 之后还外加两个模块相关的信息和类加载器名
getModuleName():StringgetModuleVersion():StringgetClassLoaderName():String
getStackTrace() 的几个弊端:注意到从 StackTraceElement 中不能直接拿到类引用(Class), 或者可以用当前线程加载器来加载 getClassName() 来获得类引用。getStackTrace() 总是返回整个线程栈的快照,即使是只关注上面几桢。为性能考虑,某些桢可能被 JVM 实现隐藏。
Java 9 如何获得线程栈信息
Java 9 为我们提供了 StackWalker , StackWalker.Option 和 StackWalker.StackFrame 类
StackWalker 有四个工厂方法 getInstance(...) , 再通过 StackWalker 的 forEach(...) 或 walk(...) 来遍历其中的 StackFrame ,
看下 StackFrame 有什么内容,
getByteCodeIndex():intgetClassName():StringgetDeclaringClass():Class getFileName():StringgetLineNumber():intgetMethodName():StringisNativeMethod(): 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 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 来输出日志所在代码位置信息,在一定程度上兴许对性能有点改善。
进群:697699179可以获取Java各类入门学习资料!
这是我的微信公众号【编程study】各位大佬有空可以关注下,每天更新Java学习方法,感谢!
学习中遇到问题有不明白的地方,推荐加小编Java学习群:697699179内有视频教程 ,直播课程 ,等学习资料,期待你的加入
网友评论