如何高效获取程序的堆栈跟踪信息?

10
我们最近应用了一个缓存解决方案,该解决方案被几乎所有应用程序模块/组件(约50个项目)使用。为了更好地理解在不同系统“位置”执行的缓存操作,我们添加了日志记录当前执行的缓存操作,包括堆栈跟踪,以确切了解触发缓存操作的内容。
我们目前的方法是这样的:我们从new Throwable()获取堆栈跟踪信息,过滤掉无关的行并记录剩余的堆栈跟踪信息。遗憾的是,创建一个新的异常记录不是一项廉价的操作。由于我们不直接使用缓存而是通过Hibernate使用它,因此很难找出哪个调用者触发了操作,而没有访问堆栈跟踪的权限。
我的问题是:是否有比Throwable().getStackTrace或Thread.currentThread().getStackTrace()更高效的解决方案来访问当前的堆栈跟踪?

3
"创建一个新的异常记录遗憾的操作并不便宜",实际上我想不出还有什么比这更昂贵的操作了。你不能从源代码中对 hibernate 进行工具化吗?" - Leo
是的,我认为我们可以。我们也讨论过这个问题,但我们想先尝试这个简单的解决方案。 - u6f6o
3个回答

18

实际上获取异常堆栈跟踪并不是很慢:

  • 只有10%的时间用于收集回溯信息 - 这在Throwable()构造函数中完成;
  • 其余90%的时间用于将回溯信息从内部格式转换为可读的StackTraceElement[]数组 - 这在getStackTrace()中完成。

这意味着,如果您不需要同步处理堆栈跟踪,只需调用new Exception()(这是一种更快的操作),然后稍后或在另一个线程中异步调用e.getStackTrace()

此外,有时候(如您的情况)并不需要完整的堆栈跟踪。您可以跳过一些堆栈框架,仅解码您感兴趣的那些。神奇的sun.misc.SharedSectets类将会有所帮助。

例如,要仅获取第2到5帧,请使用

Exception e = new Exception();
int depth = Math.min(5, SharedSecrets.getJavaLangAccess().getStackTraceDepth(e));

for (int frame = 2; frame < depth; frame++) {
    StackTraceElement elem = SharedSecrets.getJavaLangAccess().getStackTraceElement(e, frame);
    System.out.println(elem);
}

值得注意的是,sun.* 包是为内部使用而设计的,不应该被“最终”用户使用。在其他方面,我认为这意味着,在创建新的Java版本时,与正式公开的包相比,对于保持sun.*包的向后兼容性要更加谨慎。 - reallynice
6
没问题。 SharedSecrets 可以在Java 8及以下版本使用。然而,在Java 9中无法使用,但是有一个新的标准[StackWalker API](http://download.java.net/java/jdk9/docs/api/java/lang/StackWalker.html),适用于Java 9及更高版本。 - apangin

5
Thread.currentThread().getStackTrace() 实际上和 @apangin 所说的一样,但它可以在未来更快地避免创建一个Throwable。 但是,您可能会发现子采样会给您带来所需的改进。而不是记录每次访问,记录每N次访问。 如果记录每10次访问,您可以将开销减少多达90%,如果访问次数很高,则几乎与实际情况相同。另一个选择是使用像YourKit这样的分析器,它可以更有效地执行此操作。 这可以显示调用方法的不同调用者以及它们的堆栈跟踪(通常记录每10个)。

谢谢你的子采样建议!我没想到过。 - u6f6o
9
为什么您认为Thread.currentThread().getStackTrace()更快?看一下源代码:它的实现方式是new Exception().getStackTrace() - apangin
@apangin 如果可以的话,我会给你加10分。谢谢。 - Peter Lawrey

0

那样怎么避免需要获取堆栈跟踪呢? - Peter Lawrey
这个想法是使用工具接口来使用方法入口事件获取帧的位置,然后遍历堆栈。 - Damian Leszczyński - Vash

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接