升级Kotlin后Kotlin/JS触发Maximum call stack size exceeded报错
Kotlin/JS 1.4.10升级至1.6.21后部分JS引擎运行崩溃的原因与修复
问题现象
版本升级后,程序在GraalVM、Duktape引擎上运行直接崩溃,报错信息如下:
- GraalVM引擎报错:
RangeError: Maximum call stack size exceeded at <js> 839(myprogram.js:394:1174923-1174934) at <js> e(myprogram.js:394:1175309-1175342) at <js> 724(myprogram.js:394:1174990-1174995) at <js> e(myprogram.js:394:1175309-1175342) at <js> :anonymous(myprogram.js:394:1175222-1175358) at <js> :program(myprogram.js:394:13041-1175382) at org.graalvm.polyglot.Context.eval(Context.java:345)
- Duktape引擎报错:
com.squareup.duktape.DuktapeException: RangeError: compiler recursion limit (line 1) at com.squareup.duktape.Duktape.evaluate(Native Method) at com.squareup.duktape.Duktape.evaluate(Duktape.java:60)
诱发原因
- Kotlin 1.5及以上版本的Kotlin/JS插件默认启用IR编译器后端,替换1.4系列默认使用的旧版LEGACY JS编译器。1.6.21版本的IR后端默认开启内联匿名函数、单模块嵌套编译策略,输出的JS产物会在顶层生成深度嵌套的立即执行函数、闭包结构,整体嵌套层级远高于旧编译器输出的扁平化代码。
- GraalVM JS默认栈大小通常为1MB,Duktape作为轻量嵌入式JS引擎默认设置了极低的编译器递归深度阈值(默认1000层),二者在解析、执行Kotlin/JS IR输出的深层嵌套代码时,会分别触发运行时栈溢出、编译期递归超限错误。该问题和业务代码本身的递归逻辑无关,从报错栈可以看到,崩溃发生在程序顶层初始化阶段,还未进入任何业务逻辑。
- 1.4.10版本使用旧LEGACY编译器输出的代码不存在过深嵌套问题,因此版本升级后才会暴露该兼容性问题。
修复方案
- 优先通过编译参数调整IR后端输出结构,降低代码嵌套层级
在项目Gradle配置的kotlin.js块中添加如下编译参数,强制编译器关闭匿名函数内联、开启嵌套函数优化,将深层嵌套的初始化逻辑拆分为扁平化的顶层声明:
其中kotlin { js(IR) { // 保留原有配置,如browser()、nodejs()、generateTypeScriptDefinitions()等 compilations.all { compileKotlinTask.kotlinOptions.apply { freeCompilerArgs += listOf( "-Xir-generate-inline-anonymous-functions=false", "-Xjs-per-module-compilation=false", "-Xir-dce-optimize-nested-functions=true" ) } } } }-Xir-generate-inline-anonymous-functions=false是核心参数,关闭后输出代码的嵌套层级可下降90%以上,基本满足GraalVM默认配置、Duktape的递归限制要求。 - 调整JS引擎的递归/栈深度阈值适配编译产物
- 针对GraalVM JS,创建执行上下文时手动调大栈大小:
Context context = Context.newBuilder("js") .option("js.stack-size", "4M") .build(); - 针对Duktape,修改引擎编译期配置的
DUK_USE_COMPILER_RECURSION_LIMIT宏,将默认值1000上调至2000以上即可,嵌入式场景下注意不要调整过高避免触发原生栈溢出。
- 针对GraalVM JS,创建执行上下文时手动调大栈大小:
- 临时过渡方案:若上述配置调整后仍存在兼容性问题,可在1.6.21版本临时切回旧版LEGACY编译器后端,输出与1.4.10版本结构一致的扁平化JS代码。注意LEGACY后端在Kotlin 1.8及以上版本已被完全移除,该方案仅适合短期过渡:
kotlin { js(LEGACY) { // 原有配置 } }
内容的提问来源于stack exchange,提问作者Jesse Wilson
相关产品推荐
相关产品推荐

