JVM 堆栈深度:JVM 内部与通过 JNI 调用 C++

JVM stack depth: JVM internal vs C++ calling through JNI

提问人:Wheezil 提问时间:10/27/2023 最后编辑:Mark RotteveelWheezil 更新时间:11/11/2023 访问量:95

问:

在你读得太远之前,我最初的想法是不正确的。但调查很有趣。

给定一个简单的 Java 程序来测量可用的堆栈深度:

static int maxDepth = 0;

private static void foo(int depth) {
    maxDepth = Math.max(maxDepth, depth);
    foo(depth + 1);
}

public static void main(String[] args) throws Exception {
    try {
        foo(0);
    } catch (Throwable t) {
        System.out.println("Depth=" + maxDepth);
    }
}

我使用 Java 17 的默认 2MB 堆栈获得大约 20000 的最大深度。

但是,使用 JNI 从 C++ 调用 foo() 并使用 2MB 本机堆栈,导致最大深度约为 400。谁能解释这种差异?在这种情况下,JVM 是否以某种方式使用更大的堆栈帧,或者是否减少了可用的堆栈大小,或者其他什么?

我们的 C++ 到 Java 桥使用大型代码生成工具和库,因为原始 JNI 调用非常繁琐。归结为一个简单的例子有点困难,但最终调用是通过 JNI Env 进行的,例如:

cls = env->FindClass(className);
mid = mid = env->GetStaticMethodID(cls, "foo", signature);
va_list args;
va_start(args, env);
env->CallStaticVoidMethodV(cls, mid, args);

这是一个关于原生堆栈的有趣讨论

在构建了一个原生独立应用程序后,我确定问题出在我们较大的应用程序代码上,而不是我认为正在发生的事情。然而,在此过程中,我提出了一些有趣的观察:

观察 1:上面链接中讨论的“原生堆栈”,当 JVM 调用原生代码时使用,与此完全无关。正在使用的堆栈是本机 EXE 在启动时创建的堆栈。在 Windows/Visual C++ 上,这是由链接器中的“保留堆栈大小”选项设置的。在 Linux/g++ 上,我不确定这个论点,但这里讨论了它。堆栈大小越大,可用的递归深度就越深。这在通话中可见一斑。test()

观察 2:正如 @apangin 所指出的,JIT 编译器确实对最大堆栈深度有影响。可以通过运行两次测试来解决这种影响,第二次运行将使用已经编译的代码。

观察 3:如果嵌入式 JVM 生成线程,则其堆栈大小等于默认的本机堆栈大小(至少在 Windows 上是这样)。换言之,增加 Windows 链接器上的“保留堆栈大小”也会更改新 JVM 线程使用的堆栈大小。这在附加的测试调用中可见。testThread()

示例代码: 爪哇岛

package jvmtest;

public class Test1 {
  private int maxDepth;

  private void foo(int depth) {
    maxDepth = Math.max(maxDepth, depth);
    foo(depth + 1);
  }

  int test() {
    maxDepth = 0;
    try {
      foo(0);
    } catch (Throwable ex) {}
    return maxDepth;
  }

  int testThread() {
    maxDepth = 0;
    Thread t = new Thread(() -> test());
    t.start();
    try {
      t.join();
    } catch (Exception ex) {}
    return maxDepth;
  }

  public static void main(String[] args) throws Exception {
    Test1 t = new Test1();
    System.out.println("max depth=" + t.test());
    System.out.println("max depth=" + t.test());
  }
}

C++

#include <cstdlib>
#include <jni.h>
#include <cstring>
#include <iostream>
#define CLEAR(x) std::memset(&x, 0, sizeof(x))

// Set to your jar location
#define JAR_PATH "f:/temp/scratch/target/test-1.0.0-SNAPSHOT.jar";

int main()
{
  // Create the JVM
  JavaVMInitArgs vm_args;
  CLEAR(vm_args);
  JavaVMOption options[2];
  CLEAR(options);
  options[0].optionString = (char*)"-Djava.class.path=" JAR_PATH;
  vm_args.version = JNI_VERSION_1_6;
  vm_args.options = options;
  vm_args.nOptions = 1;
  JNIEnv* env = nullptr;
  JavaVM* vm = nullptr;
  jint rv = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
  if (rv != 0) {
    std::cout << "JNI_CreateJavaVM failed with error " << rv << "\n";
    ::exit(1);
  }

  // Find our test
  jclass clazz = env->FindClass("jvmtest/Test1");
  if (clazz == 0) {
    std::cout << "failed to load class\n";
    ::exit(1);
  }
  jmethodID mid = env->GetMethodID(clazz, "test", "()I");
  jmethodID midThread = env->GetMethodID(clazz, "testThread", "()I");
  jmethodID constructor = env->GetMethodID(clazz, "<init>", "()V");
  if (mid == 0 || constructor == 0) {
    std::cout << "failed to find method\n";
    ::exit(1);
  }

  // Make test instance
  auto instance = env->NewObject(clazz, constructor);
  jint result;
  // Call method using JVM thread's stack
  result = env->CallIntMethod(instance, midThread);
  std::cout << "JVM max depth=" << result << "\n";
  result = env->CallIntMethod(instance, midThread);
  std::cout << "JVM max depth=" << result << "\n";
  // Call method using native stack
  result = env->CallIntMethod(instance, mid);
  std::cout << "native max depth=" << result << "\n";
  result = env->CallIntMethod(instance, mid);
  std::cout << "native max depth=" << result << "\n";

}

当我运行示例时,我的输出是

JVM max depth=27172
JVM max depth=62489
native max depth=62477
native max depth=62477

您可以看到 JIT 的效果,因为第二个调用比第一个调用具有更深的堆栈。您还可以看到,JVM 生成的线程中可用的堆栈深度与 EXE 的 main() 线程相同。

java-native-interface jvm-hotspot

评论

0赞 user207421 10/28/2023
这是一个不同的堆栈。
0赞 Wheezil 10/28/2023
嗯,是的,这是一个不同的堆栈。它应该本机堆栈,但我不完全确定。如果不是,它是什么?我的本机堆栈与默认 JVM 堆栈的大小相同。但我已经说过了。问题是,为什么 Java 代码从入口点完全在 JVM 中执行,而不是交叉调用本机代码,从而看到可用的堆栈帧更少?
0赞 apangin 10/28/2023
后台编译可能是原因。使用 重试实验。请同时出示您使用JNI通话的密码。-Xintfoo
0赞 user207421 10/28/2023
它是本机堆栈。您正在执行本机代码。
1赞 apangin 10/28/2023
提供的信息不足以重现该问题。您是否从本机应用程序创建新的 JVM?如何将本机线程附加到 JVM?如何编译原生应用?什么是操作系统?您使用哪个 JDK 版本?请提供 MCVE

答:

0赞 Wheezil 11/11/2023 #1

答案是我错了——通过 JNI 调用的本机代码提供的堆栈深度与 JVM 自身的堆栈深度相同。但是,请参阅我在问题描述中的观察,了解有关其工作原理的一些详细信息。