Skip to content

JEP 259: Stack-Walking API | 堆栈遍历 API

摘要

定义一个高效的标准 API,用于堆栈跟踪,允许筛选堆栈跟踪中的信息并进行惰性访问。

非目标

  • 本 JEP 的目标不是将 JDK 中所有现有的堆栈跟踪代码转换为使用此新 API。

动机

没有标准 API 可以有效地遍历执行栈中选定的帧,并访问每个帧的 Class 实例。

现有 API 提供了访问线程堆栈的功能:

  • Throwable::getStackTraceThread::getStackTrace 返回一个 StackTraceElement 对象数组,其中包含每个堆栈跟踪元素的类名和方法名。

  • SecurityManager::getClassContext 是一个受保护的方法,允许 SecurityManager 子类访问类上下文。

这些 API 需要 VM 急切地捕获整个堆栈的快照,并返回表示整个堆栈的信息。如果调用者仅对堆栈顶部的几个帧感兴趣,则无法避免检查所有帧的成本。 Throwable::getStackTraceThread::getStackTrace 方法都返回一个 StackTraceElement 对象的数组,其中包含类名和方法名,但不包含实际的 Class 实例。对于对整个堆栈感兴趣的应用程序,规范允许 VM 实现省略堆栈中的一些帧以提高性能。换句话说,Thread::getStackTrace 可能返回部分堆栈跟踪。

这些 API 不满足那些当前依赖于 JDK 内部 sun.reflect.Reflection::getCallerClass 方法的使用情况,否则它们的性能开销是无法容忍的。这些用例包括:

  • 遍历堆栈,直到找到即时调用者的类。每个 JDK 调用者敏感的 API 都会在其立即调用者的类中查找,以确定 API 的行为。例如,Class::forNameResourceBundle::getBundle 方法使用立即调用者的类加载器分别加载类和资源包。反射 API,例如 Class::getMethod 使用立即调用者的类加载器来确定要执行的安全检查。

  • 遍历堆栈,过滤特定实现类的堆栈帧,以找到第一个非过滤帧。 java.util.logging API、Log4j 和 Groovy 运行时过滤中间堆栈帧(通常是实现特定和反射帧),以找到调用者的类。

  • 遍历整个堆栈,直到找到第一个特权帧为止,以查找所有保护域。这是为了进行权限检查。

  • 遍历整个堆栈,可能有深度限制。这是生成任何 Throwable 对象的堆栈跟踪和实现 Thread::dumpStack 方法所需的。

描述

此 JEP 将定义一个堆栈遍历 API,允许惰性和帧过滤,支持停止在匹配给定条件的框架的短程行走,并支持遍历整个堆栈的长程行走。

将增强 JVM 以提供灵活的机制来遍历和实现所需的堆栈帧信息,并在需要时允许有效的惰性访问附加堆栈帧。将最小化本地 JVM 转换。实现将需要线程的堆栈的稳定视图:返回一个保持堆栈指针以进行进一步操作的流无法工作,因为一旦流工厂返回,JVM 将可以重新组织控制堆栈(例如,通过取消优化)。这将影响 API 的定义。

API 将指定其在使用安全管理器时的行为,以便访问堆栈帧中的 Class 对象不会影响安全性。

建议是定义一个基于能力的 StackWalker API 来遍历堆栈。当构建 StackWalker 对象时,将在每个 StackWalker 对象上执行安全权限检查,而不是每次使用该对象时执行。它将定义以下方法:

java
public <T> T walk(Function<Stream<StackFrame>, T> function);
public Class<?> getCallerClass();

walk” 方法为当前线程打开一个“StackFrame”顺序流,然后应用具有“StackFrame”流的函数。流的拆分器以有序方式执行堆栈帧遍历。一旦“walk”方法返回,可以对“Stream<StackFrame>”对象进行一次遍历,并且在关闭时会被关闭。流在关闭后变得无效。例如,要找到过滤已知实现类的第一个调用者:

java
Optional<Class<?>> frame = new StackWalker().walk((s) ->
{
    s.filter(f -> interestingClasses.contains(f.getDeclaringClass()))
     .map(StackFrame::getDeclaringClass)
     .findFirst();
});

要快照当前线程的堆栈跟踪,

java
List<StackFrame> stack =
     new StackWalker().walk((s) -> s.collect(Collectors.toList()));

getCallerClass() 方法是为了方便找到调用者的框架,并替换 sun.reflect.Reflection.getCallerClass。使用 walk 方法获取调用者类的等价方法是:

java
walk((s) -> s.map(StackFrame::declaringClass).skip(2).findFirst());

备选方案

替代的 API 选择是使“walk”方法返回“Stream<StackFrame>”。这样的替代方案将不起作用,因为返回的流对象可能以不受控制的方式用于进一步操作。当创建堆栈帧流时,一旦流工厂返回,JVM 就可以自由地重新组织控制堆栈(例如,通过取消优化),并且没有稳健的方法来检测堆栈是否已发生变异。

相反,类似于 AccessController::doPrivileged,必须创建至少一个本机方法,该方法将建立自己的堆栈帧,然后提供对 JVM 的堆栈遍历逻辑的受控访问,用于旧帧。当此本机方法返回时,该能力必须被停用,否则以某种其他方式使其无法访问。通过这种方式,我们可以对线程自身控制堆栈的稳定视图进行有效的惰性访问。