营销号说 Java 已死第三年,我在浏览器里跑起了 JVM


营销号说 Java 已死第三年,我在浏览器里跑起了 JVM

在01的世界里探索,
在烟火人间中生活。

哈喽,大家好,我是「Yann」.

2026 年了,”Java 已死”的标题还能骗到 10w+ 阅读。

但真相是:就在上个月,Chrome 统计里 5.5% 的页面加载已经用上了 WebAssembly。Figma、Photoshop、Google Earth 全在浏览器里跑 Wasm。

而 Java,正在 Wasm 里悄悄复活。


浏览器端的”文艺复兴”

老一点的 Java 程序员,大概还记得被 Applet 支配的恐惧——安全漏洞一堆,最后被各大浏览器扫地出门。

2026 年,Java 靠着 Wasm 在浏览器里回来了。而且这次,姿势对了。

Wasm 的设计很纯粹:沙箱运行,默认零系统权限,浏览器放心,Java 也放心。目前三条技术路线已经跑出了明确的场景分化:

「CheerpJ —— “空中 JVM”,零编译即跑」

Leaning Technologies 家的拳头产品。基于完整的 OpenJDK 运行时构建,直接把 JVM 搬到浏览器里。「关键点:不需要提前编译你的 Java 代码。」 把 .jar 包丢过去就能跑,通过按需加载(on-demand loading)把运行时体积压到 10MB 左右。CheerpJ 4.0 已经支持 Java 11,Java 17 和 21 也在今年的路线图里。

「适合场景」:遗留 Java 应用迁移、企业内部系统浏览器化、不想改一行代码就想跑在浏览器里。

「TeaVM —— AOT 大剪刀,体积小到极致」

把 Java 字节码直接编译成 Wasm 或 JavaScript,编译期就做死代码清理(Tree-shaking),产物体积极小。一个 HelloWorld 级别的应用编译后几十 KB 到几百 KB 不等,启动时间在 40ms 以内。支持 WasmGC,不需要自己带垃圾回收器。

「适合场景」:对体积敏感的前端工具、需要快速启动的交互应用、游戏逻辑层移植。

「GraalVM Web Image —— 大厂正统,AOT 编译」

Oracle GraalVM 的官方方案,走 Native Image 路线编译为 Wasm。目前状态是 「experimental」(实验性),需要 GraalVM 25 早期预览版。优势是行为一致性:桌面端怎么跑,浏览器里基本怎么跑,用的是同一套 AOT 编译管线。限制也很实诚——线程、网络、图形相关 API 目前会直接抛异常。

「适合场景」:需要 GraalVM 生态、能接受实验性技术、对跨端行为一致性要求高的项目。

「最颠覆认知的地方来了。」

现场演示了一个纯前端的 Java IDE——「没有后端,100% 浏览器内运行。」 初次加载约 15MB,之后秒开。15MB 是什么概念?你随手刷一下主流新闻首页,流量可能都不止这个数。

以后带新人学 Java,别再让他配环境变量、装 IDE 了。甩一个链接过去,浏览器里直接开干。


服务端?先别急着吹

浏览器端很香,但如果有人跟你吹”Java 编译成 Wasm 在服务端/边缘端无敌”,建议先喝口水冷静一下。

现场大牛演示了两个服务端场景,结果都有点尴尬:

「场景 A:Java 调用 Wasm 模块(跨语言互操作)」

比如你在 Java 里跑一段 Rust 写的图像处理算法。把 Rust 编译成 Wasm,用 Java 的 「Chicory」 或 「GraalWasm」 加载执行——两者都是 JVM 上的 Wasm 运行时,Chicory 是纯 Java 实现、零原生依赖,GraalWasm 走 GraalVM JIT 路线性能更强。

听起来很美好,但 Wasm 的函数签名只认四种类型:「i32、i64、f32、f64」。没有字符串,没有对象,没有复杂数据结构。

你想传个 String?得自己写胶水代码:在 Wasm 的线性内存(Linear Memory)里分配空间、把字符串转成 UTF-8 字节数组 copy 进去、再把内存地址和长度作为两个 i32 传给 Wasm 函数。读回来也一样,反向操作一遍。

大牛原话:”这活儿写起来确实磨人,胶水代码部分我直接让 AI 帮我糊的。”

「场景 B:Java 跑在 Serverless 边缘端」

边缘端的系统接口标准 WASI 还在演进中(WASI 1.0 预计 2026 年底定稿)。Java 应用编译成 Wasm 跑在边缘环境里,目前对系统调用(文件、网络、定时器)的支持非常有限。

很多你习惯的 Java 标准库 API 直接不可用。想用 java.net 发 HTTP 请求?java.io 读写文件?目前大概率会抛 UnsupportedOperationException

「真实的格局是:浏览器端 Java+Wasm 已经能干活了;服务端/边缘端,工具链还在补作业。」


行业打假:那篇”Spring Boot 跑在 Wasm 边缘端”的神话

现场还点了一个国外的技术博客。那篇文章信誓旦旦地说:”我们把任意 Spring Boot 应用无缝编译成 Wasm,跑在了边缘端。”

原理写得煞有介事:先转 x86,再从 x86 转 Wasm。

「底层根本不通。」 x86 到 Wasm 不是简单的指令映射,JVM 的运行时语义(GC、线程模型、类加载)和 Wasm 的执行模型差异巨大。这种”两步转译”在工程上不成立。

大概率是一篇用 AI 生成的营销水文,抓住了”Spring Boot + Wasm + 边缘端”三个流量关键词,凑了一篇看起来很唬人的东西。

所以——「别被焦虑带节奏,也别被虚假的高潮忽悠。」

现在的真实情况是:

「浏览器端:Ready。」「服务端/边缘端:Work in Progress。」

工具在进化,路还长,但方向没错。


技术附录:Java 调用 Wasm 时,传一个 String 的真实操作

用 Chicory(纯 Java Wasm 运行时)演示:Java 向 Wasm 模块传递字符串,底层到底在做什么。

import com.dylibso.chicory.runtime.*;import com.dylibso.chicory.wasm.Parser;import java.nio.charset.StandardCharsets;/** * 场景:Java 加载 Wasm 模块(比如 Rust 写的图像处理算法), *      需要向 Wasm 传递一个字符串参数。 * * Wasm 函数签名只认 i32/i64/f32/f64,字符串必须手动塞进线性内存。 */publicclassWasmStringBridge{publicstaticvoidmain(String[] args){// 假设已加载 Wasm 模块(Rust 编译产物)var wasmStream = WasmStringBridge.class            .getResourceAsStream("/image-processor.wasm");varmodule = Parser.parse(wasmStream);var instance = Instance.builder(module).build();        String input = "Hello Yann随记 - Java → Wasm";byte[] utf8Bytes = input.getBytes(StandardCharsets.UTF_8);// 1. 在 Wasm 线性内存中分配空间//    (实际应调用 Wasm 模块暴露的 malloc 函数)        Memory memory = instance.memory();int wasmPtr = 0x100;  // 示例地址,真实场景由 alloc 返回// 2. 把字符串字节"shuffle"进 Wasm 内存        memory.write(wasmPtr, utf8Bytes, 0, utf8Bytes.length);// 3. 调用 Wasm 函数,只能传两个数字:指针 + 长度var processFn = instance.export("process_string");long[] result = processFn.apply(wasmPtr, utf8Bytes.length);        System.out.printf("✅ 已传递: ptr=%s, len=%d%n","0x" + Integer.toHexString(wasmPtr), utf8Bytes.length);// 读取返回值同理:从 result 解析指针+长度,再从内存读回字节    }}

System.arraycopy 在这里为什么不够?」 因为 Java 堆和 Wasm 的线性内存是两个独立的空间。Chicory 的 memory.write() 负责把字节从 JVM 堆拷贝到 Wasm 的隔离内存里——这一步不能省。

如果你喜欢作者,请点个赞和推荐吧~ ,我是 Yann,一个致力于用技术让全栈变简单的工程师你的每一次支持,都是我继续探索“偷懒”技术的动力。