在你读得太远之前,我最初的想法是错误的。但调查很有趣。 给定一个简单的 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++ 上,我不确定这个论点,但here对此进行了讨论。堆栈大小越大,可用的递归深度越深。这可以在
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() 线程相同。
答案是我错了——通过JNI调用的本机代码可用的堆栈深度与JVM本身的堆栈深度相同。但是,请参阅我在问题描述中的观察,了解有关其工作原理的一些详细信息。