You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:00:55