JEP 259: 栈遍历 API
概述
为栈遍历定义一个高效的标准化 API,该 API 允许对栈跟踪中的信息进行简易过滤和惰性访问。
非目标
- 目标并非将 JDK 中所有现有的栈遍历代码都转换为使用这个新 API。
动机
没有标准的 API 可以高效地遍历执行栈上的选定帧并访问每个帧的 Class
实例。
已经存在提供对线程堆栈访问的 API:
-
Throwable::getStackTrace
和Thread::getStackTrace
返回一个由StackTraceElement
对象组成的数组,这些对象包含每个堆栈跟踪元素的类名和方法名。 -
SecurityManager::getClassContext
是一个受保护的方法,它允许SecurityManager
子类访问类上下文。
这些 API 要求虚拟机(VM)积极地捕获整个堆栈的快照,并返回代表整个堆栈的信息。如果调用者只对堆栈顶部的几帧感兴趣,那么无法避免检查所有帧的开销。Throwable::getStackTrace
和 Thread::getStackTrace
方法都返回一个由 StackTraceElement
对象组成的数组,这些对象包含类名和方法名,但不包含实际的 Class
实例。对于关注整个堆栈的应用程序,规范允许虚拟机实现为了性能而省略堆栈中的某些帧。换句话说,Thread::getStackTrace
可能返回部分堆栈跟踪信息。
这些 API 无法满足目前依赖于 JDK 内部的 sun.reflect.Reflection::getCallerClass
方法的使用场景,或者它们的性能开销令人无法接受。这些使用场景包括:
-
遍历堆栈,直到找到直接调用者的类。每个 JDK 调用者敏感的 API 都会查找其直接调用者的类,以确定 API 的行为。例如,
Class::forName
和ResourceBundle::getBundle
方法分别使用直接调用者的类加载器来加载类和资源包。诸如Class::getMethod
之类的反射 API 使用直接调用者的类加载器来确定要执行的安全检查。 -
遍历堆栈,过滤掉特定实现类的堆栈帧以找到第一个未被过滤的帧。
java.util.logging
API、Log4j 和 Groovy 运行时过滤中间堆栈帧(通常是特定实现和反射帧)以找到调用者的类。 -
遍历堆栈以查找所有保护域,直到到达第一个特权帧。这是执行权限检查所必需的。
-
遍历整个堆栈,可能有深度限制。这是生成任何
Throwable
对象的堆栈跟踪以及实现Thread::dumpStack
方法所必需的。
描述
此 JEP 将定义一个栈遍历 API,该 API 支持惰性求值与帧过滤,支持在符合给定条件的帧处停止的短遍历,还支持遍历整个栈的长遍历。
JVM 将得到增强,以提供一种灵活的机制来遍历和具体化所需的栈帧信息,并在需要时允许对额外的栈帧进行高效的惰性访问。本地 JVM 转换将被降至最低。该实现需要对线程栈有一个稳定的视图:返回一个持有栈指针以供进一步操作的流将无法正常工作,因为一旦流工厂返回,JVM 就可以自由地重新组织控制栈(例如通过去优化)。这将影响 API 的定义。
API 将指定其在使用安全管理器运行时的行为,以便访问堆栈帧中的 Class
对象不会影响安全性。
该提议旨在定义一个基于功能的 StackWalker
API,用于遍历堆栈。安全权限检查将在每个 StackWalker
对象构造时执行,而不是在每次使用时执行。它将定义以下方法:
public <T> T walk(Function<Stream<StackFrame>, T> function);
public Class<?> getCallerClass();
walk
方法为当前线程打开一个 StackFrame
的顺序流,然后将函数应用到 StackFrame
流上。该流的分割迭代器(spliterator)会以有序的方式执行堆栈帧的遍历。Stream<StackFrame>
对象只能被遍历一次,并且会在 walk
方法返回时关闭。一旦流被关闭,就无法再使用。例如,要从已知的实现类列表中过滤出第一个调用者,可以这样做:
Optional<Class<?>> frame = new StackWalker().walk((s) ->
{
s.filter(f -> interestingClasses.contains(f.getDeclaringClass()))
.map(StackFrame::getDeclaringClass)
.findFirst();
});
要捕获当前线程的堆栈跟踪快照,
List<StackFrame> stack =
new StackWalker().walk((s) -> s.collect(Collectors.toList()));
getCallerClass()
方法是为了方便查找调用者的帧,它是 sun.reflect.Reflection.getCallerClass
的替代方法。使用 walk
方法获取调用者类的等效方式是:
walk((s) -> s.map(StackFrame::declaringClass).skip(2).findFirst());
替代方案
另一种 API 选择是让 walk
方法返回 Stream<StackFrame>
。然而,这种替代方案并不可行,因为返回的流对象可能会以一种不受控制的方式被进一步操作。当创建一个堆栈帧的流时,一旦流工厂返回,JVM 就可以自由地重新组织控制堆栈(例如,通过去优化),并且没有可靠的方法来检测堆栈是否已被修改。
相反,与 AccessController::doPrivileged
类似,必须创建至少一个本地方法,该方法将建立自己的栈帧,然后为更早的栈帧提供对 JVM 栈遍历逻辑的受控访问。当此本地方法返回时,该功能必须被停用,或者以其他方式使其不可访问。通过这种方式,我们可以在对线程自身控制栈的稳定视图上,高效地实现对栈帧的惰性访问。