系统级语言 · AOT · LLVM · 自举

superJ

读起来像 Java
跑起来像 C//

一门单线程的提前编译语言,属于 Java 家族 — 但不是 Java 的子集。没有 VM,没有 JIT,没有垃圾回收器。熟悉的类、泛型与异常;底层是原生速度与 arena 作用域内存。

0 个垃圾回收器 0 个运行时线程 编译为原生代码 编译器用 SuperJ 编写
event.sj
// 看起来像 Java — 运行时没有 GC。
public class Report {
  public static void main(String[] args) {
    int total = 0;
    arena scratch {              // 作用域内存
      int[] rows = new int[1<<16];
      for (int i = 0; i < rows.length; i++)
        total += (rows[i] = i * 3);
    }                             // 在这里释放 — O(1),无停顿
    System.out.println(total);
  }
}

核心主张

保留让 Java 用起来舒服的部分,丢掉妨碍原生性能的部分。

SuperJ 编译为 LLVM IR 再生成机器码。没有字节码,没有类加载,没有追踪式回收器,也没有线程 — 整个程序在编译期就完全确定,所以行为可预测到每一次分配。

内存

Arena,不是 GC

一个全局 arena 给整个程序用,再加上词法 arena {} 块 — 退出时一次 O(1) 释放,没有 stop-the-world 停顿。

后端

通过 LLVM 生成原生代码

直接生成优化后的机器码。没有 VM,没有 JIT 预热,没有要打包的运行时。

模型

单线程

线程、锁、管程和内存模型都消失了 — 从结构上就是确定性的。

自举

自托管

SuperJ 编译器用 SuperJ 编写 — 工具链端到端自举。我们用自己的语言打造自己的工具链。

招牌特性

确定性的内存 — 编译器还能证明它是安全的

arena 块里分配短生命周期对象;块退出时整个 arena 一次性释放。如果一个引用的生命周期会超出它所在的 arena,那就是一个编译错误,而不是潜伏的 use-after-free。

arena.sj
Node createNode(int v) {
  return new Node(v);   // 全局 arena — 可以返回
}

void process() {
  arena temp {
    Buffer buf = new Buffer(1024);  // 在 temp 中
    Node n = createNode(5);       // 在全局 arena
    // outer = buf;  ← 被拒绝:逃逸出它的 arena
  }              // temp 释放:buf 被回收,n 仍然有效
}

词法的,不是动态的。 一个 new 属于它书写所在的那个 arena — 被调用的方法仍然使用全局 arena,除非它们自己开了 arena。

arena temp { … } 分配中…
temp
Buffer 1 KB
byte[] payload
Frame ×3
✓  整个 arena 一次 O(1) 释放 — 无扫描,无停顿

↳  n(全局 arena)在块退出后仍然存活。

系统能力

Java 触不到的底层控制力

在 arena 模型之外,SuperJ 还提供了那些你原本得退回 C 才能写出来的原语 — 同时类型系统依然在背后替你把关。

内存

Arena 与栈作用域

arena {} 实现 O(1) 批量回收;local 实现栈作用域引用。

数值

宽整数

一等公民 i128i256i512 原语,为加密与大数运算而生。

布局

固定布局结构体

无头的值类型,有线格式上确定的布局 — 协议与 mmap 的理想选择。

数组

矩形数组

int[,] — 连续存储、按步长索引,和锯齿数组 int[][] 是不同的类型。

互操作

原生 C / Rust

native 方法直接调用 C ABI — 没有 JNI,没有编组层。

函数

方法引用

Class::method 指向一个单抽象方法接口 — 无捕获,没有闭包。

泛型

擦除泛型

类级与方法级,带边界参数,以及 ? extends / ? super 通配符。

数据并行

SIMD 向量

可移植的向量族(Byte64Float8、…),lowering 为原生 SIMD IR。

安全

明确赋值

一个局部变量在赋值前被读取就是编译错误 — 不会意外读到垃圾值。

取舍

一份深思熟虑的账本

每一次移除都换来可预测性;每一项保留都是当初让 Java 高效的部分。

✗ 从 Java 移除

  • // 线程、synchronizedvolatile
  • // 追踪式垃圾回收器
  • // 反射与类加载
  • // lambda 与闭包
  • // 自动装箱 / 拆箱
  • // 非静态内部类
  • // 受检异常、模块、finalize()

✓ 从 Java 保留

  • // 类、接口、继承
  • // 带擦除与通配符的泛型
  • // 枚举与匿名类
  • // 异常(全部为非受检)
  • // 可变参数与方法引用
  • // 熟悉的控制流与语法
  • // String / string,统一的不可变类型

证明

原生速度,平坦的尾部

一个跑在内置 HTTP 协议栈上的 Web 服务器,编译成单一原生二进制,在回环节点上加压测试 — 服务器钉在一个核上,压力发生器钉在另一个核上。它的响应速度比 wrk 发请求的速度还快。

515K
请求 / 秒 — wrk 上限约 423K,瓶颈在它身上
~1.1×
p99 在 p50 的 ~1.1× 以内 — 尾部平坦
0
GC 停顿 — arena 内存,无需扫描

测量环境:Ryzen 9 9950X3D,服务器与客户端各占一核(服务器在 CPU 4,客户端在 CPU 3):SuperJ 自己的流水线客户端能达到 ~515K req/s,而 wrk 顶到 ~423K — 瓶颈是压力发生器,不是服务器。在持续压力下服务器的尾部保持平坦 — p50 156µs、p99 167µs — 因为没有回收器会停顿。查看完整基准测试 →

一句话,整个想法

读起来像 Java。跑起来像 C。
所以是 superJ。

没有 VM。没有 JIT。没有意外。

下载 SuperJ 阅读快速入门