JVM堆栈深度:JVM内部与通过JNI调用的C++之间的区别

3
在你继续阅读之前,我先声明一下,我的最初想法是错误的。但是这个调查很有趣。
给定一个简单的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()调用中可以看到。
示例代码: Java
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()线程相同。

这是一个不同的堆栈。 - undefined
是的,这是一个不同的堆栈。它应该是本地堆栈,但我并不完全确定。如果不是的话,那是什么呢?我的本地堆栈与默认的JVM堆栈大小相同。但我已经说过了。问题是,为什么在Java代码完全在JVM中执行,而不是跨调用本地代码的情况下,可用的堆栈帧较少? - undefined
这是本地堆栈。您正在执行本地代码。 - undefined
@apangin 我在C++中添加了一个关于该方法的简介。不幸的是,我还没有将其简化为一个独立的示例。 - undefined
1
提供的信息不足以重现问题。您是否从本机应用程序中创建了一个新的JVM?您如何将本机线程附加到JVM?您如何编译本机应用程序?操作系统是什么?您使用的JDK版本是多少?请提供一个MCVE - undefined
显示剩余4条评论
1个回答

0
答案是我错了 - 通过JNI调用的本地代码可用的堆栈深度与JVM自身的堆栈深度相同。但是,请参阅我在问题描述中的观察结果,了解一些关于这个问题的详细信息。

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