知序
返回
保存草稿
发布
H1
H2
H3
1
2
3
Excalidraw 图
AI 写作
AI 优化
目录
输入内容后显示目录
Markdown
# 01 - 什么是 GraalJS > 这篇解决什么问题:你想在 Java 应用里跑一段 JavaScript——或者让某段业务逻辑能"热更新不重启",又或者把一段不可信脚本关进笼子里执行——但一搜全是"Nashorn 已移除、GraalJS 是替代品"这类零散信息。你到底该不该用、它和 Node 到底什么关系、凭什么说它是现在最靠谱的答案?这篇把一个引擎讲透,不藏原理,也不灌概念。 > 读完你会知道:GraalJS 的本质与定位、它接替 Nashorn 的完整来龙去脉、与 Node.js 相比真正的差异化价值、Truffle/Graal JIT 让它性能接近 V8 的机制,以及它给你(Java 开发者)带来的能力边界。 --- ## 1. 先把它是什么说清楚 ### 1.1 一句话定义 GraalJS 是 **GraalVM 官方提供的 JavaScript 引擎**。拆开看它有三个内核特征,缺一不可: | 特征 | 说明 | |---|---| | 出身 | GraalVM 官方出品,**不是第三方 JS 实现** | | 实现 | 基于 **Truffle 语言实现框架**,热点代码交由 **Graal JIT** 编译为机器码 | | 标准 | 完整兼容 **ECMAScript 2023**,ES Modules、async/await、Optional Chaining、Nullish Coalescing 等现代语法全部支持 | > 一句话:**GraalJS = GraalVM 官方的高性能 JS 引擎,运行在普通 JVM 里,性能靠 Truffle 框架 + Graal JIT 逼近 V8。** ### 1.2 最关键的定位:它"跑在哪"决定了它的一切 这是整个课程最重要的一张图,请务必看懂它——**它决定了 GraalJS 和所有其他 JS 运行时的根本不同**: ```text ┌─────────────────────────────────────────────────┐ │ Java 应用 │ │ │ │ ┌───────────────────────────────────────────┐ │ │ │ Polyglot API(Context / Value) │ │ ← 推荐入口,本课 04 篇 │ │ 或 javax.script(ScriptEngine) │ │ ← 兼容层,本课 03 篇 │ └───────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────┐ │ │ │ GraalJS 引擎 │ │ │ │ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ Truffle AST 解释器 │ │ │ 源码→AST→逐节点解释 │ │ └─────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────┐ │ │ │ │ │ Graal JIT 编译器 │ │ │ 热点节点→机器码 │ │ └─────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ JVM(宿主运行时) │ └─────────────────────────────────────────────────┘ ``` **读图的三个要点**: 1. **它是"跑在 JVM 进程里"的引擎**,不是独立运行时。这是它与 Node.js 之间最本质的分水岭。 2. **应用侧有两套入口**:`Polyglot API`(新、推荐)和 `javax.script`(兼容老代码)。两套入口底层是同一颗 GraalJS 引擎。 3. **引擎内部是"Truffle 解释 + Graal JIT 编译"两段式**:先解释执行拿行为,再对热点编译。这套机制就是它性能接近 V8 的原因,下面会专门讲。 > 一句话:**GraalJS 是嵌在 Java 进程里的引擎——这是它 all 的出发点。** ⚠️ 补充一个常见误解:有人以为 GraalJS 需要一个特殊的"GraalVM JDK"才能跑。**不需要**——它作为普通 jar 依赖就能跑在标准 JDK 17+ 上(这正是后文会讲到的选型要点;真正会踩的坑是用了一个"没装 js 组件"的 GraalVM JDK,反而报错,详见踩坑篇)。 --- ## 2. 为什么是它:Nashorn 被移除的完整来龙去脉 理解"为什么是 GraalJS",得先理解它取代谁、以及为什么那个"谁"活不下去。 ### 2.1 Java 内置 JS 引擎的演进史 ```text JDK 6-7: Rhino(Mozilla 开源,性能差,只到 ES3) JDK 8: Nashorn 引入(性能大幅提升,仍停在 ES5 时代) JDK 11: Nashorn 标记 @Deprecated JDK 15: Nashorn 正式移除(JEP 372) 之后: GraalJS 成为官方推荐替代 ``` ### 2.2 Nashorn 为什么被移除(三个核心原因) 1. **ECMAScript 规范演进太快**:ES6(2015) 之后几乎每年一版,新增语法与语义是个无底洞。Nashorn 团队选定了 "ES5 兼容" 这个目标后,与现代 JS 的差距每年都在拉大——到它被移除时,项目里跑不了 `const`/`let`,更别提 async/await。 2. **维护资源持续吃紧**:跟语言规范更新本来就是高成本投入,而在 Oracle 当时的优先级里,维护一套 JS 实现的 ROI 过低。 3. **GraalJS 已成为更成熟的替代**:Truffle 框架让"在新语言里实现一门语言"的成本大幅下降,GraalJS 在同一条底子上连同一套 Polyglot 互操作体系,可以共用生态、共用投入,是更长远的路。 > 一句话:**不是 GraalJS 抢了 Nashorn 的位子,而是 Nashorn 跟不上 ECMAScript 的脚步自己掉队,GraalJS 接棒成为官方推荐。** 关注点应从"引擎叫什么"转向"引擎能不能跟上现代 JS 标准"。 --- ## 3. 它和 Node.js / Nashorn 到底差在哪 "都是跑 JS,我为什么不直接用 Node?" 这可能是每个 Java 开发者第一次听到 GraalJS 都会问的问题。答案是**场景**,而不是性能。 | 维度 | GraalJS | Node.js (V8) | Nashorn(已移除) | |---|---|---|---| | 运行位置 | **Java 进程内**(embedded) | 独立运行时 | Java 进程内 | | 与 Java 互操作 | 原生(Polyglot API) | 需额外桥接(RPC/HTTP/child_process) | 原生(但 API 老化) | | 沙箱能力 | 强:可细粒度禁 IO / 线程 / 进程 / 宿主访问 | 弱:默认即宿主级权限 | 弱 | | 性能(JIT 预热后) | 接近 V8 | 基准 | 约为 GraalJS 的 1/2~1/3 | | 启动 & 预热成本 | 首次执行较慢(要收集 profile) | 快 | 快 | | 典型定位 | Java 应用内嵌脚本引擎 | 独立后端服务 / 运行时 | 历史遗留 | **为什么"性能接近 V8"却仍选 GraalJS?** 因为对"跑在 Java 进程内且要互操作"这个诉求,性能是次要约束——你把 JS 交给 Node,Java 与 Node 之间得靠网络协议来桥,那一跳的序列化/传输开销早就盖过了引擎本身的性能。**让 JS 待在同一个进程内存里、直接传 Java 对象、直接调 Java 方法,才是 GraalJS 的价值。** > 一句话选型:**你要在 Java 进程内执行 JS、要互操作和沙箱 → 选 GraalJS;你要独立的后端服务/超大生态 → 选 Node.js。两者是用途差异,不是性能竞争。** --- ## 4. 深一层:Truffle + Graal JIT 凭什么让性能接近 V8 这是很多人只知其然而不知其所以然的地方,也决定了你对 GraalJS 的性能心智。把"接近 V8"这句结论背后的机制讲透。 ### 4.1 Truffle 框架是"怎么让新语言跑得快"的底座 写一个 JS 引擎,最朴素的做法是"解释 AST":读源码 → 建语法树 → 逐节点解释执行。简单,但慢。Truffle 提供的是**自优化解释器(self-optimizing interpreter)**——它让你在解释执行的同时收集类型信息,让解释器自身"会进化": - 一开始:按通用路径解释每个节点 - 跑了几次后:对某个节点记住了**它实际遇到的类型/形状**,直接走专用快路径(specialization) - 一旦某假设(比如"这里一直是 number 相加")被打破,再 fallback 回慢路径 这段机制的意义:**GraalJS 的优势不在"解释 JS"本身,而在背后有 Truffle 这样为高性能语言实现铺路的框架。** ### 4.2 Graal JIT 把热点代码编成机器码(核心) 光有"自优化解释"还不够接近 V8。真正的引擎是: 1. **第一遍:Truffle 解释执行**,同时引擎记录每个节点的运行次数与类型分布(profile)。 2. **启动阶段之后:Graal JIT 介入**——对达到热点阈值的 AST 子树做**部分求值(partial evaluation)**,把"解释器对这个节点的调度逻辑"去掉,**只保留用户 JS 真正要做的运算**,编译成优化的机器码炸回去。 3. **去优化(deopt)兜底**:若运行时遇到 profile 里没见过的输入,抛弃这批特化机器码,回到解释执行重新积累,再重新编译。 > 💬 **Q:那 GraalJS 是不是"冷启动很慢、跑久了才快"?** > > 是的,这是它作为 JIT 引擎的固有特征。thực 上: > > | 场景 | 表现 | > |---|---| > | 长跑型脚本(服务器上长期驻留的业务脚本) | 预热后接近 V8,GraalJIT 的价值完全发挥 | > | 短生命周期脚本(每次请求执行一条规则、执行完就丢) | 预热成本可能大于收益,性能优势不明显 | > > 这条边界直接关系到"GraalJS 适合怎么用"——后面规则引擎篇会专门谈短脚本要不要复用 Context 做预热。**先记住:GraalJS 的"快"是有条件的。** > 一句话:**GraalJS 的性能来自 Truffle 自优化 + Graal JIT 部分求值把热点 JS 编译成机器码,是典型 JIT,优势在长跑、短脚本收益有限。** --- ## 5. 认识它能带给你的能力边界 在正式写代码前,先给"能力边界"定个坐标,让你知道学了它能干什么、不该指望它干什么: | 能力 | 说明 | 本课在哪 | |---|---|---| | 在 JVM 里执行现代 JS | ECMAScript 2023 全支持 | 04 篇 | | Java 传数据给 JS、JS 算完 Java 取回 | Value / Proxy 类型系统 | 05/06/08 篇 | | JS 反向调用 Java 对象方法 | 需显式授权宿主访问 | 07 篇 | | 沙箱隔离不可信脚本 | 禁 IO/线程/进程/宿主,只留纯计算 | 10 篇 | | 异常统一包装 | PolyglotException 收口一切 | 09 篇 | **不该指望 GraalJS 做的**:用它替代 Node 跑独立服务、用它做浏览器 JS、指望它"一行 API 搞定某业务"。它是个**引擎/工具**,不是服务框架。 > 一句话:**GraalJS 给你的是"在 Java 进程内安全地执行并互操作 JS"这个能力集合,不是一套业务框架。** --- ## 6. 最小的可感知示例:跑通并取回 不贴一堆配置文件,看看"让 Java 真正执行 JS 并拿回值"最小长什么样。下面已经假设你的工程引入了 GraalVM 的 polyglot 与 js 能力(引入方式见课程其它篇,这里聚焦引擎本身)——这是后续 Context API 篇的 preview: ```java try (Context context = Context.create("js")) { // 建一个 JS 引擎上下文 Value result = context.eval("js", "40 + 2"); // 执行 JS 表达式 int n = result.asInt(); // 把 JS 值转成 Java int System.out.println(n); // 42 } ``` **这段代码的三层含义**(现在不求全懂,建立感知即可): 1. `Context.create("js")` —— 创建隔离引擎,`try` 块结束自动关闭,接管生命周期。 2. `context.eval("js", "40 + 2")` —— 执行一段 JS,返回 `Value` 包装。 3. `result.asInt()` —— 跨语言边界拿回的不是原生 `int` 而是 `Value`,需要显式转型。**这在 GraalJS 里是个贯穿始终的原则:一切跨语言值都是 `Value`,先 `isXxx` 判断、再 `asXxx` 转换**(05 篇系统讲)。 > 一句话:**最小接入 = 建 Context → eval 执行 JS → asXxx 取回。三行,跑通。** --- ## 7. 引出的疑问:Context 和 ScriptEngine 是什么关系? 看到 `Context.create("js")` 你多半会想:那 Java 老接口 `ScriptEngine` 呢?两者在 Java 侧是**两套门面**,底层同一颗 GraalJS 引擎: | API | 来源 | 定位 | 推荐度 | |---|---|---|---| | `javax.script.ScriptEngine` | JDK 标准 | Nashorn 时代的兼容写法,迁移成本最低 | 老项目迁移用 | | `org.graalvm.polyglot.Context` | GraalVM 原生 | 功能完整、性能更好、沙箱可控 | **新项目首选** | 一句话决策:**新项目用 `Context`;手上有一堆 Nashorn 老代码想平滑搬过来,才考虑 `ScriptEngine` 兼容层。** 这两套我们在 03/04 两篇分别展开。 > 一句话:**`Context` 是推荐的新入口,`ScriptEngine` 是为老代码迁移留的门。多数情况下,你只需要 `Context`。** --- ## 8. 本篇小结 ```text ┌──────────────────────────────────────────────────────┐ │ 01 什么是 GraalJS 要带走的话 │ ├──────────────────────────────────────────────────────┤ │ 1. GraalJS = 嵌在 JVM 里的官方 JS 引擎,普通 JDK 17+ │ │ 2. 靠 Truffle 自优化 + Graal JIT 把热点 JS 编成机器码 │ │ 3. 性能"接近 V8"但有 JIT 预热边界:长跑型才显优势 │ │ 4. 完胜 Nashorn,接棒成为官方推荐替代 │ │ 5. 价值 = 进 JVM + 双向互调 + 沙箱(Node 给不了) │ │ 6. 最小接入 = Context → eval → asXxx,三行跑通 │ └──────────────────────────────────────────────────────┘ │ v 已经建立了"它是什么、凭什么"的整体认知 ──→ 下一步深入学习它的双入口 ``` ## 思考题(自测) 1. 如果 Java 应用只要"跑一段隔离的纯计算脚本",用 GraalJS 和调 Node.js 子进程各有什么代价?哪种更划算? 2. Truffle 的"解释执行 → 预热 → JIT 编译"模式,对短生命周期脚本(如每次请求执行一条规则)有什么影响?怎么缓解? 3. JavaScript 每年更新,GraalJS 把支持的 ECMAScript 版本"固定在 2023"。对应用来说,升级引擎版本到底意味着什么、什么时候该担心?
预览
发布文章
添加摘要让读者更快了解文章内容
取消
确认发布
AI 写作助手
输入一个主题,AI 将为你生成一篇 Markdown 博客文章
AI 正在写作中...
取消
开始生成
AI 优化助手
描述你的修改需求,AI 将在原文基础上进行补充或微调
AI 正在优化中...
取消
开始优化
画布
从已有图里选
✕
图名
保存后生成 /images/excalidraw/<图名>.svg(只能用字母、数字、短横线、下划线、点)
取消
保存并插入