Brand logo
HomeHardwarePolicy & RegulationSoftwareCommunicationsConsumer ElectronicsGeneral
  1. Home
  2. /
  3. software
  4. /
  5. openldk-common-lisp-jvm-spring-boot-84ms

MyTechDailyNews Software Deep-Dive: OpenLDK, a Common Lisp JVM Runtime That Starts a Spring Boot App in about 84 milliseconds

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.

Why the project exists, in two sentences

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.

The compilation pipeline, step by step

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.

Polyglot source written directly inside Lisp forms

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:

  1. Clojure takes ten primes from an infinite lazy sequence: 2 3 5 7 11 13 17 19 23 29
  2. Groovy squares them with the spread-dot operator and sums: 2397
  3. Kawa adds one with Java's Long.sum, by colon notation, and divides exactly by the first of 6, 5, 4, 3, 2 that does not divide evenly, escaping the search with call/cc: 1199/3, returned as numerator and denominator
  4. JScheme sums each one's digits in a named let: 20 3
  5. Scala matches the pair against a list pattern with a guard: 60
  6. Kotlin pours it into a local data class, copies it, and sweetens it with when and the Elvis operator: 61
  7. ABCL writes it with FORMAT, in Roman numerals and in English: LXI (sixty-one)

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).

Method combination and classes that Java believes are its own

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.

Failure handling, collections and build wiring

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.

Software already running on OpenLDK

  • Java's own javac compiler, with the build producing a warmed javacl executable
  • Clojure, with protocols, records, atoms and futures
  • Kawa and JScheme running Scheme
  • Scala's compiler and REPL; Kotlin compiling source into executable class files; Groovy running dynamic scripts and statically compiled classes
  • ABCL, whose loop is especially pleasing: Common Lisp first compiled into Java bytecode, translated back into Common Lisp, then compiled by SBCL, with its REPL, compiler, CLOS, conditions and Java interop running inside that environment
  • Spring Boot and Quarkus applications, including Spring PetClinic with embedded Tomcat, Hibernate and H2 - separate from the small command-line application used for the startup measurements
  • A CommandLineRunner demo built on auto-configuration, configuration properties bound into a record, dependency injection and an auto-configured Jackson mapper, printing Spring's banner and logging through Logback

Warm images versus snapshots, and how that differs from CRaC

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:

ApproachWhat is savedWhat still happens at launchWhat you must handle
OpenLDK warm imageCompiled code for exercised pathsApplication initialization, plus compilation of paths warmup skippedWarmup coverage of your real startup route
OpenLDK snapshot imageCompiled code plus the initialized object graphMain is called again with the Spring context already presentDoes not implement the CRaC API or preserve running threads; classpath files and JDK must be available at runtime
CRaC / Coordinated Restore at CheckpointA checkpointed JVM processRestore of the saved processCheckpoint-based JVM tooling rather than SBCL image saving
GraalVM native imageAn ahead-of-time binarySpring initialization at launchClosed-world assumption: all classes known at build time, dynamic accesses covered by analysis or metadata

Published startup numbers

Configuration quoted from the author's recent run:

  • Host: Intel(R) Core(TM) Ultra 7 255H, 16 CPUs
  • JDK: openjdk version "25.0.4.1" 2026-08-18
  • GraalVM: native-image 25.0.4.1.1 2026-08-18
  • OpenLDK: 39f33c0
  • Load average before: 3.75 5.29 3.83
  • Twenty timed runs per variant
VariantMedianMinMaxPeak RSS
HotSpot, fat jar2138.3 ms2078.9 ms2219.0 ms266 MB
HotSpot, extracted jar1791.3 ms1755.0 ms1880.1 ms225 MB
HotSpot + Spring AOT1481.4 ms1420.5 ms1575.8 ms194 MB
HotSpot + JDK AOT cache758.8 ms713.4 ms828.8 ms181 MB
HotSpot + Spring AOT + AOT cache595.1 ms568.5 ms625.6 ms157 MB
GraalVM native image53.5 ms50.9 ms63.1 ms41 MB
OpenLDK warm image1261.7 ms1237.8 ms1283.2 ms450 MB
OpenLDK snapshot image83.9 ms77.5 ms88.6 ms175 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.

Compatibility, deployment and risk notes

  • OpenLDK still has compatibility gaps, including unimplemented bytecode verification.
  • Development targets SBCL on Linux, so validate your own platform before adopting.
  • Dynamic behavior survives launch: because OpenLDK keeps its class loader and JIT compiler, new classes and previously unused methods can still compile after startup - and those paths pay their compilation cost when first used.
  • Saved images need their classpath files and JDK available at runtime; the image documentation covers deployment constraints.
  • Thread pools and other process-local resources need attention when designing an application for snapshot reuse.
  • Nothing here changes your data-handling obligations; you are running the same libraries and the same application code, just on a different compiler and runtime.

Try the polyglot demo in four steps

  1. Meet the build requirements, then work from the OpenLDK checkout.
  2. Run ocicl install.
  3. Run make sbcl-polyglot.
  4. Run ./sbcl-polyglot --load examples/one-form.lisp - seven languages later, the console prints LXI (sixty-one).

Common misconceptions

  • "A fast alternative will displace HotSpot." The project's own position is that HotSpot will usually be much faster for general Java execution; OpenLDK's value is making Java libraries feel at home in Common Lisp.
  • "Saving an image removes all runtime work." Lazy compilation remains: paths that warmup never exercised still compile on first use.
  • "Warm image and snapshot are the same thing." A warm image keeps compiled code with static state reset by default; only :reset-classes nil carries initialized application state.
  • "Faster startup means a faster service." The benchmark window closes at process exit; throughput and readiness were not measured.
  • "This is CRaC in another language." It is similar in spirit only - no CRaC API, no preserved threads.

FAQ for software teams

Which JVM-hosted languages can I try today?

Java classes plus Kotlin, Clojure, Scala, Groovy, Kawa, JScheme and ABCL, each in its own syntax within one Lisp process.

Do I have to rebuild my sources for it?

No. The pipeline starts from ordinary class files and takes its runtime libraries from JDK 25 via JMOD or lib/modules.

Can I inspect or patch a running Spring Boot application?

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.

How are overloaded methods distinguished?

Instance methods become methods of CLOS generic functions named by their JVM signatures, so overloads remain separate.

What does the language interop cost in code changes?

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.

What is the single number worth remembering?

The snapshot median of 83.9 ms - the figure behind the headline.

Actionable takeaways

  • Treat snapshotting as an application-design decision: keep a static ConfigurableApplicationContext, start Spring only when it is null, and choose :reset-classes nil deliberately.
  • Build your warmup list around the code paths you actually launch, then re-measure both time and peak RSS.
  • Compare against GraalVM native image, Spring AOT and JDK AOT cache before committing to any cold-start strategy.
  • Check the missing bytecode verification and the SBCL-on-Linux target against your deployment matrix first.
  • Prototype the interop layer on one library, and list dependencies explicitly in your ASDF and Maven components.

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.

Brand logo

Tech Daily

Your Daily Source for Consumer Tech, Policy & Communications

HardwarePolicy & RegulationSoftwareCommunicationsConsumer ElectronicsGeneralAPAC EdmGrowth

© 2026 Tech Daily.

Cookie Notice

We use cookies to improve your experience and analyze website traffic. Read our Privacy Policy to learn more.