【问题标题】:Is there a way to know maximally reached JVM call stack depth for a particular program run?有没有办法知道特定程序运行的最大达到 JVM 调用堆栈深度?
【发布时间】:2018-09-05 13:05:54
【问题描述】:

我今天一直在编写一个递归函数,递归深度取决于输入长度。

我想从纯粹的兴趣的角度来看,是否有某种方法可以监视特定程序执行期间的最大调用堆栈深度(可能在某些 JVM 日志或其他地方)?

经过一番思考,我可以想象一种分析方法来近似计算它,但这将非常耗时,并且需要对 JVM 内部结构和字节码有相当好的了解。

JVM 允许配置堆栈大小内存的限制,但我从未见过有关如何获得实际达到限制的任何信息,而不是内存大小单位,而是分配的堆栈帧数。

【问题讨论】:

  • 简短的回答是......不。
  • 更长的答案是......是的。可以轻松地制作处理MethodEntry/MethodExit 事件的JVMTI 代理来跟踪当前堆栈深度。一个不太精确但几乎没有开销的替代方案是使用采样分析器,例如async-profiler.
  • 如果您真的对给定程序的堆栈深度和输入数据感兴趣,您可以系统地运行每个线程配置不同堆栈大小的程序,直到您发现导致程序的堆栈大小失败。
  • 既然你在谈论堆栈大小内存,看起来你不是在追求最大递归数,而是你的程序所需的堆栈内存量。坏消息是,it is entirely non-deterministic。当然,您可能想知道在 JVM 选项中指定确切的堆栈内存量会给您带来什么,但对我来说,拥有一个具有堆自动内存管理功能的 JVM,但堆栈大小固定,在需要时无法扩展,无论如何都是不合时宜的。你应该非常小心递归算法......
  • @Holger System.out.println(System.identityHashCode("Hello World")); 每次从头开始运行程序时都会打印相同的数字。

标签: java jvm


【解决方案1】:

可以轻松地制作 JVMTI 代理,该代理将跟踪 MethodEntry / MethodExit 事件并相应地增加或减少堆栈深度计数器。这是此类代理的示例。当程序结束时,它会打印记录的最大 Java 堆栈深度。

#include <jvmti.h>
#include <stdint.h>
#include <stdio.h>

static volatile int max_depth = 0;

static int adjust_stack_depth(jvmtiEnv *jvmti, int delta) {
    intptr_t depth = 0;
    (*jvmti)->GetThreadLocalStorage(jvmti, NULL, (void**)&depth);
    (*jvmti)->SetThreadLocalStorage(jvmti, NULL, (const void*)(depth + delta));
    return (int)depth;
}

void JNICALL MethodEntry(jvmtiEnv *jvmti, JNIEnv* jni, jthread thread, jmethodID method) {
    adjust_stack_depth(jvmti, +1);
}

void JNICALL MethodExit(jvmtiEnv *jvmti, JNIEnv* jni, jthread thread, jmethodID method,
                        jboolean was_popped_by_exception, jvalue return_value) {
    int depth = adjust_stack_depth(jvmti, -1);
    if (depth > max_depth) {
        max_depth = depth;  // TODO: replace with atomic CAS to avoid race condition
    }
}

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv* jvmti;
    (*vm)->GetEnv(vm, (void**)&jvmti, JVMTI_VERSION_1_0);

    jvmtiCapabilities capabilities = {0};
    capabilities.can_generate_method_entry_events = 1;
    capabilities.can_generate_method_exit_events = 1;
    (*jvmti)->AddCapabilities(jvmti, &capabilities);

    jvmtiEventCallbacks callbacks = {0};
    callbacks.MethodEntry = MethodEntry;
    callbacks.MethodExit = MethodExit;
    (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));

    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, NULL);
    (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_METHOD_EXIT, NULL);

    return 0;
}

JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
    printf("Max stack depth = %d\n", max_depth);
}

编译:

gcc -fPIC -shared -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -o libmaxdepth.so maxdepth.c

运行:

java -agentpath:/path/to/libmaxdepth.so MyProgram

但是,跟踪每个方法的进入和退出是非常昂贵的。一个不太准确但更有效的替代方案是采样分析器,它定期记录正在运行的线程的堆栈跟踪,例如async-profiler 或 Java Flight Recorder。

【讨论】:

  • 但是嵌套调用的数量根本无法帮助您估计所需的堆栈内存。所以这只是一个没有意义的数字。
  • @Holger,我准确地询问了分配的堆栈帧的数量。可能它在实际的生产生活中不是很有用,但它确实提供了一个有趣的洞察力,了解事情是如何在幕后工作的:)
  • @AlexanderArendar 实际上,这与“事情如何在幕后工作”相反;您将获得嵌套方法调用的正式数量,即与 Java 源代码中编写的调用数量相匹配,精确地破坏任何会阻止创建堆栈框架或使调用无法检测到的优化。这就是为什么,正如这个答案所说,“跟踪每个方法的进入和退出非常昂贵”。它不再像平常那样做。
  • @apangin,我实际上已经编译过这个并试过了。因此,我希望在使用具有空 main 方法的最简单程序进行测试时看到 Max stack depth = 1 输出。但实际输出是Max stack depth = 30,如果我从main 添加一些类实例化和额外的方法调用,它保持不变。你能告诉我我做错了什么来测试这个吗?
  • @AlexanderArendar 这里没有错。在启动main 之前,JVM 运行应用程序启动器代码,该代码加载并验证主类。启动器也是用 Java 编写的,所以在这种情况下,代理基本上会计算启动器代码的最大深度。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-06-19
  • 2021-12-28
  • 1970-01-01
  • 2010-12-24
  • 2018-02-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多