JNI调用Java方法时JVM栈深度差异原因探究
栈深度差异问题调查记录
前置说明
最初的猜想有误,但相关调查过程颇具价值。
问题背景
以下是一段用于测量可用栈深度的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在此场景下使用了更大的栈帧,还是可用栈空间被缩减,或是其他因素?
JNI调用的核心代码如下:
cls = env->FindClass(className); mid = env->GetStaticMethodID(cls, "foo", signature); va_list args; va_start(args, env); env->CallStaticVoidMethodV(cls, mid, args);
调查结论与观察
通过构建原生独立应用后发现,问题源于大型应用代码本身,而非最初猜想的JVM或JNI机制问题。调查过程中得到以下有趣结论:
- 观察1:JNI调用Java方法时使用的是原生EXE启动时创建的栈,而非JVM调用原生代码时的专用栈。在Windows/Visual C下,可通过链接器的“reserve stack size”选项设置栈大小;Linux/g下也有对应编译参数调整栈大小。栈越大,递归深度上限越高,这可在
test()方法的调用中体现。 - 观察2:JIT编译器会影响最大栈深度。可通过连续运行两次测试规避该影响,第二次运行会使用已编译的优化代码。
- 观察3:嵌入式JVM创建线程时,其栈大小等于原生程序的默认栈大小(至少Windows平台如此)。也就是说,增大Windows链接器的“reserve stack size”选项,也会同步改变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主线栈大小一致。
内容的提问来源于Stack Exchange,提问作者Wheezil
相关产品推荐
相关产品推荐

