Java也能秒变编译器!用LLVM JIT从0到1手搓Hello World


Java+LLVM+FFM,无JNI、无C代码,全程Java手搓IR,JIT秒出原生指令,终端Hello World炸裂输出,编译器技能GET!全程高能无尿点,看完直接跪!

2025年最硬核的Java Advent玩法来了!往年大家都在字节码里卷来卷去,今年直接掀桌子——不用javac、不用字节码、更不用JNI去跪舔C++,我们就用纯Java,把LLVM中间码撸出来,再让LLVM自己的JIT引擎把它编译成原生指令,当场跑给你看!终端里那一声“Hello, World!”不是打印出来的,是Java亲手搓出来的机器码吼出来的!

看完这篇,你也能在简历上写“曾用Java写过一个编译器”,面试官直接给你倒茶!



先给小白五分钟补课:LLVM到底是啥?为什么Java要蹭它?

LLVM,全称“Low Level Virtual Machine”,听着像虚拟机,其实它是编译器界的乐高积木!

苹果家的clang、Rust的rustc、Swift的编译器,全是它亲儿子。
它最牛的地方在于“中间表示”——LLVM IR,一种介于高级语言和汇编之间的超级SSA(静态单赋值)语言,写起来像汇编,却能跨平台优化。

今天咱们不弄clang,就用Java的Foreign Function & Memory API(FFM,JDK22+新宠,JNI的终极替代)去调戏LLVM的C API,让Java直接生成IR,再让LLVM JIT把IR炸成x86机器码,跑在自家JVM门口,爽感堪比在星巴克里用豆汁儿做手冲!



环境一把梭:Ubuntu秒装LLVM 20,jextract一键生成Java绑定,Maven三分钟搭好舞台!

第一步,装LLVM 20,别怂,一行命令:  

wget https://apt.llvm.org/llvm.sh && chmod +x llvm.sh && sudo ./llvm.sh 20  

装完头文件+so全齐活。接着去GitHub扒官方jextract,Linux解压即用。新建Maven项目,pom里把source/target怼到25(对,就是要玩最新版),然后jextract闪亮登场:  

jextract -l LLVM-20 -I /usr/include/llvm-c-20 -t com.example.llvm --output src/main/java --header-class-name LLVM /usr/include/llvm-c-20/llvm-c/Core.h …(后面一堆头文件)  

哒哒!几百个Java方法瞬间生成,C的宏、常量、函数全变Java静态方法,MemorySegment、MethodHandle安排得明明白白,JNI时代彻底拜拜!



内存段Arena:Java的“堆外内存黑卡”, native世界畅通无阻!

FFM API的灵魂是MemorySegment,而Segment必须在Arena里分配。Arena就像高级版malloc,try-with-resources自动释放,内存泄漏?不存在的!本文全程用Arena.ofConfined(),线程私有、轻量、快, arena.allocateFrom("Hello, World!") 直接把Java字符串塞进堆外内存,UTF-8零结尾,LLVM看了都说舒服。

后续全局变量、函数指针、JIT地址全在这块“黑卡”里蹦迪,Java第一次把内存玩成这么丝滑!



起手式:创建LLVM模块,打印空IR,先让lli报错给你看!

代码走起:  

try (Arena arena = Arena.ofConfined()) {  
    var module = LLVMModuleCreateWithName(arena.allocateFrom("hello"));  
    var llvmIrCharPtr = LLVMPrintModuleToString(module);  
    System.out.println(llvmIrCharPtr.getString(0));  
    LLVMDisposeMessage(llvmIrCharPtr);  
    LLVMDisposeModule(module);  
}  

运行输出只有三行:  
; ModuleID = 'hello'  
source_filename = "hello"  

拿管道喂给lli:  

java -cp … com.example.App | lli  

lli怒吼:Symbols not found: [ main ]  

对,模块空得能跑火车,接下来咱就给它灌灵魂!



函数签名大保健:int main() 安排!先声明再定义,LLVM仪式感拉满!

继续加料:  

var int32Type = LLVMInt32Type();  
var mainType = LLVMFunctionType(int32Type, NULL, 0, 0);  
var mainFunc = LLVMAddFunction(module, arena.allocateFrom("main"), mainType);  

再打印IR,看到declare i32 @main(),lli依旧喊找不到入口,因为这只是函数“标题”,没有“正文”——basic block!



基本块BasicBlock:控制流图节点,函数体第一块砖头必须码齐!

LLVMAppendBasicBlock(mainFunc, arena.allocateFrom("entry"))

瞬间创建entry块,此时IR里函数从declare变成define,但块里空空如也,lli报错:expected instruction opcode  
好,马上塞指令!



指令Builder:LLVM里的“打字机”,指哪打哪,一行指令一杯咖啡!

LLVMCreateBuilder() 创建builder,LLVMPositionBuilderAtEnd(builder, entry) 把光标挪到entry块末尾,先来条return:  
LLVMBuildRet(builder, LLVMConstInt(int32Type, 0, 0))  

再跑lli,安静如鸡,echo $? 返回0,世界首次清净!但咱要的是“Hello World”,不是“Hello空气”!


全局字符串常量:
@str = private constant [14 x i8] c"Hello, World!\00",Java一行代码自动搞定!

LLVMBuildGlobalStringPtr(builder, arena.allocateFrom("Hello, World!"), arena.allocateFrom("hello_str"))  

这行神操作背后:LLVM自动在模块顶层插入@hello_str,类型[14 x i8],零结尾,private unnamed_addr,对齐1,打印IR肉眼可见!但光声明不调用,等于把奶茶封盖却不给吸管!



外部函数声明:puts老铁来自libc,先告诉LLVM“哥们长这样”!

puts签名:int puts(char*),于是:  
var charPtrType = LLVMPointerType(LLVMInt8Type(), 0);  
var putsParamTypes = arena.allocate(ADDRESS, 1);  
putsParamTypes.set(ADDRESS, 0, charPtrType);  
var putsType = LLVMFunctionType(int32Type, putsParamTypes, 1, 0);  
LLVMAddFunction(module, arena.allocateFrom("puts"), putsType)  

模块里瞬间多了一行:declare i32 @puts(ptr)  

此刻,Java像极了一个认真写符号表的编译器前端,帅到没朋友!



终极CALL:LLVMBuildCall2,把hello_str塞进puts,终端炸裂输出!

创建实参数组:  

var callArgs = arena.allocate(ADDRESS, 1);  
callArgs.set(ADDRESS, 0, helloStr);  
LLVMBuildCall2(builder, putsType, putsFunc, callArgs, 1, arena.allocateFrom("puts"))  

指令插入完毕,IR里entry块出现:  

call i32 @puts(ptr @hello_str)  
ret i32 0  

管道喂lli,终端啪一声:Hello, World! 全网观众起立鼓掌!


JIT加速:lli只是解释器,咱要原生指令狂飙!MCJIT启动,x86机器码秒生成!

五步初始化:  

LLVMLinkInMCJIT();  
LLVMInitializeX86Target();  
LLVMInitializeX86TargetInfo();  
LLVMInitializeX86TargetMC();  
LLVMInitializeX86AsmPrinter();  
LLVMCreateJITCompilerForModule(jitCompiler, module, 2, errorPtr)  

优化等级2,编译器直接拉满!随后LLVMGetPointerToGlobal(executionEngine, mainFunc) 拿到函数指针,地址塞进Java的MemorySegment,准备起飞!



MethodHandle黑科技:Java直接调用编译后的main,原生入口一毫秒都不耽误!

Linker.nativeLinker().downcallHandle(addressOfMainFunc, FunctionDescriptor.of(JAVA_INT))  

返回一个MethodHandle,签名等于int main(),Java端直接:  

int result = (int) functionHandle.invoke();  
System.out.println("main() returned: " + result);  

运行!终端先出Hello, World! 再出main() returned: 0  

全程无lli、无clang、无gcc,Java一人分饰三角:前端+优化器+JIT司机,帅到模糊!



彩蛋时间:把优化等级开到3,再塞点循环向量化和内联,你能让Hello World跑出SPEC2017的既视感!

LLVM IR支持

attribute("noinline")、attribute("alwaysinline")、llvm.memset.p0i8.i64 
各种内置,你可以用Java生成斐波那契、矩阵乘法、甚至tiny ray tracer,然后JIT编译,性能直追C++。

FFM API还能对接SIMD、AVX512,未来Valhalla vector API一旦合体,Java将拥有官方支持的“编译器+向量化+JIT”三位一体大杀器,提前布局的你就等于提前拿到通往次世代高性能计算的船票!