Cold-start budgets for JVM tools just got a new comparison point, and it is not a vendor announcement. A saved image of an already-initialized Spring Boot application relaunches in about 84 milliseconds under OpenLDK, a Common Lisp project whose JIT compiler and runtime consume class files, hand the translated code to SBCL for native compilation, and leave Java objects living alongside Lisp data in one process and heap. Below, the Software desk's read for people who actually maintain such workloads: what the compilation pipeline does lazily, how polyglot source blocks behave, when a snapshot is worth engineering for, which published timings you may safely quote, and the compatibility gaps to check before you schedule a migration.
Many years ago, its author worked on gcj, the GNU Compiler for the Java Programming Language, an alternative Java compiler and runtime. In late 2024, the same developer wanted to reach a Java library from SBCL and found no integration style that suited the application, which pointed toward a small JVM bytecode processor able to run code directly in process; work started before coding agents became useful for this kind of task, and the first working version was hand-written.
In the last few days the project moved well past that origin, hosting several JVM-hosted languages and saving a live Spring Boot context. Its stated aim is library integration, not a speed war: when the job is general Java execution, HotSpot will usually be much faster.
Everything begins with ordinary class files. OpenLDK targets JDK 25 and relies on OpenJDK's runtime libraries, taking them from the JDK's JMOD files or the lib/modules container. Class loading builds a CLOS representation and installs stubs for its methods; the first call to a stubbed method kicks off compilation, moving bytecode into an intermediate representation, then Lisp, then native code through SBCL, at which point the compiled definition replaces the stub.
Laziness is the design point that matters most for footprint. Reading in a library is not the same as compiling every method in it, because a large library carries a lot of code that a particular application never uses. Take a trivial method, public static int add(int x, int y) { return x + y; }: strip the generated control-flow scaffolding and exception machinery and the translated body reduces to (sb-c::mask-signed-field 32 (+ x y)), where the signed-field operation keeps Java's 32-bit integer overflow behavior. Given typed operands, SBCL turns that into a machine addition. Java semantics survive while the heavy lifting stays in the compiler SBCL already provides.
The same translation makes the running program reachable from Lisp. Fields become slots; instance methods become methods of CLOS generic functions named by their JVM signatures, which keeps overloaded methods separate; static methods become ordinary Lisp functions; Java's exception hierarchy maps to a corresponding Lisp condition hierarchy. Compiled code accumulates in a persistent cache that later runs can reuse whenever the classes it depends on are unchanged, entries are separated by OpenLDK build, and different images can also reuse compiled JDK methods.
Foreign source can appear straight in your Lisp code. After starting the relevant languages and enabling OpenLDK's source syntax, a Kotlin expression can sit inline: (let ((n 4)) # @kt{ (1..$n) . map { it * it } }) evaluates to (1 4 9 16). The $n splice hands a Lisp value into the Kotlin code as an object, and lexical variables, collections and callbacks cross the boundary without serialization into source strings. The same mechanism supports Scala, Groovy, Clojure, Kawa, JScheme and ABCL, each block keeping the language's own syntax; Clojure additionally has a #[ ... ] notation. A Clojure function can return as a callable foreign-function object that the surrounding Lisp invokes with ordinary funcall while the interop layer converts arguments and results, and the reverse direction works too: Lisp functions become callbacks for other languages.
The file examples/one-form.lisp carries the heading "One form, seven languages" and demonstrates seven runtimes sharing one Lisp process, in an inside-out chain:
In each block the source is written just as that language writes it. The $ before a nested block passes its result to the enclosing language as an object, so the expression crosses seven implementations using their own syntax and libraries inside one Lisp process; run ./sbcl-polyglot --load examples/one-form.lisp and the console prints LXI (sixty-one).
CLOS integration reaches into live code. Defining a :before method for "java.util.ArrayList" with the signature "clear()V" lets Lisp print the number of elements immediately before Java's ArrayList.clear() empties the list; in that notation V means the method returns Java's void, while size()I returns an integer. Calls coming from compiled Java, Kotlin, Scala or Groovy pass through it as well, because Java methods take part in CLOS method combination, so Lisp can supply :before, :after and :around behavior where the application needs it.
OpenLDK's define-java-class goes the other direction: it produces a class file installed through the application class loader while the method bodies are Lisp code. Such classes can implement Java interfaces, and their methods can consult Lisp state and call other language components while Java reflection still sees an ordinary Java class.
Errors follow the same integration. A NumberFormatException escaping a foreign call becomes the typed Lisp condition foreign-number-format-error, which is also a parse-error, with analogous mappings covering arithmetic, file, type and storage errors. The foreign-call interface provides use-value and retry restarts, so a Lisp handler can supply a replacement value or retry a failed foreign call; a Lisp error raised inside a callback travels the other direction as an appropriate Java exception, and if it escapes back to Lisp the original Lisp condition is recovered. Each side keeps using its own error-handling facilities.
Collections get similar accommodations: a Java list can be exposed as a Lisp sequence, converting elements as they are accessed, and a Lisp sequence or hash table can be presented to Java through a backed list or map view. On the build side, ASDF systems can contain Maven JAR components and source components for the supported languages. Kotlin, Scala and Groovy files compile during ASDF's compilation operation and their classes become available to later components; Maven components put libraries on the shared classpath, and dependencies must currently be listed explicitly. Inline Kotlin and Scala blocks can also compile with the surrounding Lisp file when their splices have static types and the language compiler is running; their class bytes are stored in the FASL, so loading that FASL does not require recompiling those blocks. The interop documentation covers these interfaces and the language-specific conversions.
All that compilation has a cost. Booting a large language implementation from cold can take minutes, because OpenLDK is compiling the implementation itself while it runs, so saving executable images makes a substantial difference. A warm image runs the application during the build, exercises selected paths and saves the compiled code; paths warmup never exercised still compile on first use. Application state is a separate choice: by default dump-warm-image resets application-class static state so initialization can happen again, while :reset-classes nil preserves the state left by the warmups - which is what an application designed to reuse that state needs.
The snapshot entry point keeps the initialized context: static ConfigurableApplicationContext ctx, where main starts Spring if ctx is null and later calls reuse the context and execute its runners. After loading OpenLDK into SBCL and setting CLASSPATH to the application classes and each required JAR, the image can be built as boot-snapshot for demo.boot.Snapshot, with the classpath taken from CLASSPATH, :warmups (list nil nil) and :reset-classes nil; the first warmup initializes Spring, the second exercises the reuse path, and saving the image preserves the initialized object graph.
The spirit is close to CRaC (Coordinated Restore at Checkpoint): initialize the application once, save its state, and reuse that work on subsequent launches. The mechanics are not the same:
| Approach | What is saved | What still happens at launch | What you must handle |
|---|---|---|---|
| OpenLDK warm image | Compiled code for exercised paths | Application initialization, plus compilation of paths warmup skipped | Warmup coverage of your real startup route |
| OpenLDK snapshot image | Compiled code plus the initialized object graph | Main is called again with the Spring context already present | Does not implement the CRaC API or preserve running threads; classpath files and JDK must be available at runtime |
| CRaC / Coordinated Restore at Checkpoint | A checkpointed JVM process | Restore of the saved process | Checkpoint-based JVM tooling rather than SBCL image saving |
| GraalVM native image | An ahead-of-time binary | Spring initialization at launch | Closed-world assumption: all classes known at build time, dynamic accesses covered by analysis or metadata |
Configuration quoted from the author's recent run:
| Variant | Median | Min | Max | Peak RSS |
|---|---|---|---|---|
| HotSpot, fat jar | 2138.3 ms | 2078.9 ms | 2219.0 ms | 266 MB |
| HotSpot, extracted jar | 1791.3 ms | 1755.0 ms | 1880.1 ms | 225 MB |
| HotSpot + Spring AOT | 1481.4 ms | 1420.5 ms | 1575.8 ms | 194 MB |
| HotSpot + JDK AOT cache | 758.8 ms | 713.4 ms | 828.8 ms | 181 MB |
| HotSpot + Spring AOT + AOT cache | 595.1 ms | 568.5 ms | 625.6 ms | 157 MB |
| GraalVM native image | 53.5 ms | 50.9 ms | 63.1 ms | 41 MB |
| OpenLDK warm image | 1261.7 ms | 1237.8 ms | 1283.2 ms | 450 MB |
| OpenLDK snapshot image | 83.9 ms | 77.5 ms | 88.6 ms | 175 MB |
Scope matters before you quote these figures anywhere: the recorded times cover process launch through exit for the small command-line application, peak RSS is measured separately, the benchmark script confirms that application output matches across variants, and the test measures startup plus a short task, not steady-state throughput or web-server readiness.
Read across the columns and GraalVM is faster and smaller here, 53.5 ms and 41 MB against OpenLDK's 83.9 ms and 175 MB, but the two get there differently - GraalVM still performs Spring initialization at launch, while OpenLDK reuses the saved context. Every HotSpot configuration trails badly: the fat jar median is 2138.3 ms, Spring AOT alone 1481.4 ms, JDK AOT cache alone 758.8 ms, and Spring AOT combined with an AOT cache 595.1 ms, with the extracted jar at 1791.3 ms and the warm image at 1261.7 ms in between.
Java classes plus Kotlin, Clojure, Scala, Groovy, Kawa, JScheme and ABCL, each in its own syntax within one Lisp process.
No. The pipeline starts from ordinary class files and takes its runtime libraries from JDK 25 via JMOD or lib/modules.
Yes - Emacs and SLY can be connected to a Spring Boot application running on OpenLDK, letting you inspect objects and monkey-patch behavior while the application keeps running.
Instance methods become methods of CLOS generic functions named by their JVM signatures, so overloads remain separate.
Splices pass values as objects rather than strings, so lexical variables, collections and callbacks cross the boundary directly; the interop documentation lists the language-specific conversions.
The snapshot median of 83.9 ms - the figure behind the headline.
For MyTechDailyNews Software readers, the practical reading is that OpenLDK now occupies a slot neither HotSpot nor GraalVM fills: a live, inspectable, patchable process that also executes Java, with a saved-state relaunch cheap enough to change how you think about command-line and short-lived workloads. The startup time is the hook, but the pieces most likely to change your daily work are the persistent code cache, the inline polyglot blocks and the CLOS hooks into library methods. Expect rough edges, and test on your own JDK 25 setup before drawing conclusions.
Written by Sarah Chen for MyTechDailyNews. Corrections and reader questions to the Tech Daily editorial desk: hello@techdaily.com.