【问题标题】:Accessing caller information quickly快速访问来电者信息
【发布时间】:2016-11-05 20:03:11
【问题描述】:

我正在研究一个 aspectj 方面,它需要知道从哪里调用它。目前我正在使用

new Throwable().getStackTrace();

要访问此信息,但每个方面都需要数百微秒才能运行。

我查看了 SecurityManager 但似乎只能让我获得类名。

还有其他我错过的选择吗?

更新

我对@apangin 的回答的评论中提到了 JMH 基准测试结果:

Benchmark                       Mode  Cnt      Score    Error  Units
MyBenchmark.javalangaccess13i   avgt  100   2025.865 ±  8.133  ns/op
MyBenchmark.javalangaccess2i    avgt  100   2648.598 ± 24.369  ns/op  
MyBenchmark.throwable1          avgt  100  12706.978 ± 84.651  ns/op

基准代码:

@Benchmark
public StackTraceElement[] throwable1() {
    return new Throwable().getStackTrace();
}

@SuppressWarnings("restriction")
@Benchmark
public static StackTraceElement javalangaccess2i() {
    Exception e = new Exception();
    return sun.misc.SharedSecrets.getJavaLangAccess().getStackTraceElement(e, 2);
}

@SuppressWarnings("restriction")
@Benchmark
public static StackTraceElement javalangaccess13i() {
    Exception e = new Exception();
    return sun.misc.SharedSecrets.getJavaLangAccess().getStackTraceElement(e, 13);
}

在戴尔 XPS13 9343(i5-5200U @ 2.2GHz)上的 Windows 10、JDK 1.8.0_112 下运行测试

【问题讨论】:

  • 显而易见的问题是你为什么需要这个。由于各种原因,获取堆栈跟踪需要相当长的时间,因此最好的办法是找到一个不涉及读取堆栈跟踪的原始问题的替代解决方案。
  • 获取来电者信息很慢。例如,每个日志框架都有这个问题。他们都建议你如果需要性能,不要打开这些信息。
  • 正如其他 cmets 所说:您错过的是您需要以某种方式解决的相互矛盾的要求;因为您找不到一种合理、可靠的方式来满足所有这些要求。
  • shipilev.net/blog/2014/exceptional-performance 中包含了一些关于像这样获取堆栈跟踪的性能的详细信息 - 在这方面它可能没有“帮助”,因为它主要是说“是的,它很贵!” .但也许仍然很有趣。
  • 你看过 InvocationHandler 吗?它可能不适用于您的结构,但值得一试。

标签: java performance aspectj stack-trace


【解决方案1】:

不幸的是,Throwable.getStackTrace() 似乎是在纯 Java 8 中获取调用者框架的唯一可行选择。

但是,有一个特定于 JDK 的技巧可以只访问一个选定的堆栈帧。
它使用非标准的sun.misc.SharedSecrets API。

public static StackTraceElement getCaller() {
    Exception e = new Exception();
    return sun.misc.SharedSecrets.getJavaLangAccess().getStackTraceElement(e, 2);
}

这里 2 是所需帧的索引。

这在最新的 JDK 8 之前都可以正常工作,但在 JDK 9 中将无法访问私有 API。好消息是 Java 9 将有新的标准 Stack-Walking API。以下是如何在 Java 9 中执行相同操作。

public static StackWalker.StackFrame getCaller() {
    return StackWalker.getInstance(Collections.emptySet(), 3)
            .walk(s -> s.skip(2).findFirst())
            .orElse(null);
}

JVMTI GetStackTrace 函数也适用于旧版本和新版本的 Java。但它需要链接本机代码。

【讨论】:

  • 非常感谢这个非常完整的答案。我已经通过 JMH 对您的建议进行了一些测试,似乎 Throwable.getStackTrace() 大约需要 13us,而您建议的 SharedSecrets api 需要 2~3us,具体取决于访问的项目。我还打算测试 Java 9 api 的完整性,但它需要对 Eclipse 进行工具更改才能使其正常工作,我对打破它感到紧张。但github.com/pingtimeout/stack-walker-benchmark 的基准表明它的速度较慢。
  • 看起来 SharedSecrets api 的持续时间可能比我最初的测试表明的变化更大。将两次调用的总和放入应用程序后,我看到的值高达 20us(一个异常值超过 600us)。
  • 我想我找到了一个基于 AspectJ 的更好的解决方案。
【解决方案2】:

您在谈论 AspectJ。因此,您不需要任何反射,只需使用板载 AspectJ 方法,例如 thisEnclosingJoinPointStaticPart.getSignature() 结合 call() 切入点:

驱动程序应用:

package de.scrum_master.app;

public class Application {
    private static final long NUM_LOOPS = 1000 * 1000;

    public static void main(String[] args) {
        Application application = new Application();

        long startTime = System.nanoTime();
        for (long i = 0; i < NUM_LOOPS; i++)
            application.doSomething();
        System.out.printf(
            "%-40s  |  %8.3f ms%n",
            "AspectJ thisEnclosingJoinPointStaticPart",
            (System.nanoTime() - startTime) / 1.0e6
        );

        startTime = System.nanoTime();
        for (long i = 0; i < NUM_LOOPS; i++)
            application.doSomething2();
        System.out.printf(
            "%-40s  |  %8.3f ms%n",
            "Throwable.getStackTrace",
            (System.nanoTime() - startTime) / 1.0e6
        );

        startTime = System.nanoTime();
        for (long i = 0; i < NUM_LOOPS; i++)
            application.doSomething3();
        System.out.printf(
            "%-40s  |  %8.3f ms%n",
            "SharedSecrets.getJavaLangAccess",
            (System.nanoTime() - startTime) / 1.0e6
        );
    }

    public void doSomething() {}
    public void doSomething2() {}
    public void doSomething3() {}
}

方面:

package de.scrum_master.aspect;

import de.scrum_master.app.Application;
import sun.misc.SharedSecrets;

public aspect MyAspect {
    before() : call(* Application.doSomething()) {
        Object o = thisEnclosingJoinPointStaticPart.getSignature();
        //System.out.println(o);
    }

    before() : call(* Application.doSomething2()) {
        Object o = new Throwable().getStackTrace()[1];
        //System.out.println(o);
    }

    before() : call(* Application.doSomething3()) {
        Object o = SharedSecrets.getJavaLangAccess().getStackTraceElement(new Throwable(), 1);
        //System.out.println(o);
    }
}

控制台日志:

AspectJ thisEnclosingJoinPointStaticPart  |     7,246 ms
Throwable.getStackTrace                   |  1852,895 ms
SharedSecrets.getJavaLangAccess           |  1043,050 ms

如您所见,AspectJ 比第二好的基于反射的方法快约 140 倍。

顺便说一句,如果你取消注释方面的打印语句,你会看到这三种类型的输出:

void de.scrum_master.app.Application.main(String[])
de.scrum_master.app.Application.main(Application.java:16)
de.scrum_master.app.Application.main(Application.java:21)

享受吧!

【讨论】:

  • +1 关于 AspectJ 内置功能的要点!不幸的是,它有严重的局限性。仅提供直接调用者的静态位置。有时您需要更深入地查看几帧(例如,当从实用方法调用观察方法时)。它也不尊重反射调用和通过方法引用的调用。
  • 好吧,你没有提到这些限制。在正常情况下,我的解决方案非常快。无论如何,这对我来说只是一个手指练习。我想知道为什么你真的需要来电者信息。商业价值在哪里?
  • 我不是原始问题的提问者 :) 也许,OP 对您的解决方案绝对没问题。我之前也遇到过类似的问题,在我的例子中,有趣的调用者在下面大约 4 帧。 JVMTI 似乎是获得它的最快方法。
  • 糟糕,这是真的。你不是 OP,我的错。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-28
  • 2016-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-16
  • 2019-02-25
  • 1970-01-01
相关资源
最近更新 更多