Brand logo
HomeCloud ComputingSecuritySoftware DevelopmentAI ApplicationsStartupsGeneral
  1. Home
  2. /
  3. softwaredevelopment
  4. /
  5. openldk-common-lisp-jvm-spring-boot-84ms

OpenLDK Adoption Notes for Software Teams: Common Lisp, JVM Bytecode, Polyglot Sources, and Spring Boot Startup in about 84 milliseconds

A runtime integration question can be timed in milliseconds: how short can a Spring Boot relaunch become after compilation and saved state have already done the heavy work? The answer includes about 84 milliseconds because OpenLDK can save a Spring Boot image and relaunch it in about 84 milliseconds. The background begins with an older JVM tooling path: Many years ago, gcj, the GNU Compiler for the Java Programming Language, was explored as an alternative Java compiler and runtime. The near-term trigger was narrower. In late 2024, the work required a Java library inside SBCL, and available integration styles did not fit. A hand-written JVM bytecode processor, created before coding agents were useful enough for this kind of work, runs inside the process. In the last few days, the image-saving path reached the startup result above.

In current shape, OpenLDK is a JIT compiler and runtime for Java written in Common Lisp. It reads Java bytecode, converts that bytecode to Lisp, and lets SBCL compile the Lisp into native machine code. Java objects share the same process and heap as the Lisp application, so Java libraries can feel at home in Common Lisp. HotSpot remains usually much faster for general Java execution. The integration surface is unusual enough to be useful: Java classes turn into CLOS classes; Lisp can place methods around methods written in Kotlin; a Clojure function can become something you pass to funcall; Emacs and SLY can inspect objects and monkey-patch behavior while an application keeps running.

From class file to native code

OpenLDK starts from ordinary class files. It targets JDK 25 and relies on OpenJDK's runtime libraries, taking them from the JDK's JMOD files or its lib/modules container. When a class loads, OpenLDK creates its CLOS representation and installs stubs for its methods. The first call to a method starts compilation: bytecode becomes an intermediate representation, then Lisp, then native code through SBCL. The compiled definition replaces the stub.

Lazy loading is central to the build model. Loading a library does not mean every method in it is compiled. A large library may contain much code that a particular application never uses. Consider a Java method: public static int add(int x, int y) { return x + y; }. After removing generated control-flow scaffolding and exception machinery, the translated body reduces to (sb-c::mask-signed-field 32 (+ x y)). The 32-bit operation preserves Java's 32-bit integer overflow behavior. With typed operands, SBCL can turn this into a machine addition. OpenLDK preserves Java's semantics while using the compiler SBCL already provides.

The same translation makes the program reachable from Lisp. Java fields become slots. Instance methods become methods of CLOS generic functions, named by their JVM signatures so overloaded methods remain separate. Static methods become ordinary Lisp functions. Java's exception hierarchy maps to a corresponding Lisp condition hierarchy. Compiled methods are kept in a persistent code cache. Later runs can load cached code when the classes it depends on are unchanged, and cache entries are separated by OpenLDK build. Different images can also reuse compiled JDK methods.

Polyglot source blocks inside Lisp files

Foreign source can appear directly inside Lisp source. After starting the relevant languages and enabling OpenLDK's source syntax, a Kotlin expression can be written as (let ((n 4)) # @kt{ (1..$n) . map { it * it } }), and the result is (1 4 9 16). $n passes the Lisp value into the Kotlin code as an object. Lexical variables, collections, and callbacks can cross the boundary without serialization into source strings. The same mechanism supports Scala, Groovy, Clojure, Kawa, JScheme, and ABCL. Each block keeps the language's own syntax. Clojure also has a #[ ... ] notation. A Clojure function can return as a callable foreign-function object. The surrounding Lisp calls it with ordinary funcall, and the interop layer converts arguments and results. The reverse direction works too: Lisp functions can become callbacks for other languages.

Nested blocks are possible. The file examples/one-form.lisp carries the heading One form, seven languages, and it demonstrates seven languages sharing one Lisp process. The comments describe an inside-out chain. Clojure takes ten primes from an infinite lazy sequence: 2 3 5 7 11 13 17 19 23 29. Groovy squares them with the spread-dot operator and sums: 2397. Kawa adds one with Java's Long.sum, by colon notation, and divides exactly by the first of 6, 5, 4, 3, 2 that doesn't divide evenly, escaping the search with call/cc: 1199/3, returned as numerator and denominator. JScheme sums each one's digits in a named let: 20 3. Scala matches the pair against a list pattern with a guard: 60. Kotlin pours it into a local data class, copies it, and sweetens it with when and the Elvis operator: 61. ABCL writes it with FORMAT, in Roman numerals and in English: LXI (sixty-one). In each block, the source is written in that language, just as that language writes it. The $#@lang{ ... } form, or $#[ ... ] for Clojure, splices the value of the block inside into the language around it, passed as an object. Running ./sbcl-polyglot --load examples/one-form.lisp prints LXI (sixty-one). The $ before a nested block passes its result to the enclosing language. The expression crosses seven language implementations, using their own syntax and libraries, inside one Lisp process.

CLOS integration and Java-visible classes

CLOS integration gives teams a way to instrument Java calls from Lisp. Lisp can add a method that runs before Java's ArrayList.clear() by defining a :before method for java.util.ArrayList and the signature clear()V. In that signature, V means the method returns Java's void, while size()I returns an integer. The added method prints the number of elements before the original implementation clears the list. Calls from compiled Java, Kotlin, Scala, or Groovy code pass through it as well. Java methods take part in CLOS method combination, so Lisp can provide :before, :after, and :around behavior where the application needs it.

Lisp can also define a class that Java code sees as an ordinary Java class. OpenLDK's define-java-class produces a class file and installs it through the application class loader, while the method bodies are Lisp code. These classes can implement Java interfaces. Their methods can consult Lisp state and call other language components, while Java reflection still sees an ordinary Java class.

Failure handling, data views, and build wiring

Error handling participates in the same integration. A NumberFormatException escaping a foreign call becomes a typed Lisp condition, foreign-number-format-error, which is also a parse-error. Other mappings cover arithmetic, file, type, and storage errors. The foreign-call interface provides use-value and retry restarts. Lisp handlers can supply a replacement value or retry a failed foreign call. A Lisp error raised inside a callback can travel the other direction as an appropriate Java exception. If it escapes back to Lisp, the original Lisp condition is recovered. Each side can use its own error-handling facilities.

Collections have accommodations too. A Java list can be exposed as a Lisp sequence, converting elements as they are accessed. A Lisp sequence or hash table can be presented to Java through a backed list or map view. The build system joins in. 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; their classes become available to later components. Maven components put libraries on the shared classpath. 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. Loading that FASL does not require recompiling those blocks. The interop documentation covers these interfaces and the language-specific conversions.

Programs and frameworks already running

These features sit on top of substantial programs already running. Java's javac compiler works on OpenLDK, and the build produces a warmed javacl executable. Clojure runs with protocols, records, atoms, and futures. Kawa and JScheme run Scheme. Scala's compiler and REPL work; Kotlin compiles source into executable class files; and Groovy runs dynamic scripts and statically compiled classes. ABCL contributes an especially pleasing loop: Common Lisp first compiled into Java bytecode, translated back into Common Lisp, then compiled by SBCL. Its REPL, compiler, CLOS, conditions, and Java interop run inside that environment.

There are also Spring Boot and Quarkus applications. Spring PetClinic runs with embedded Tomcat, Hibernate, and H2. That is separate from the small command-line application used for the startup measurements below.

Warm images, snapshots, and Spring Boot reuse

All that compilation has a cost. Starting a large language implementation cold can take minutes, because OpenLDK is compiling the implementation itself while it runs. Saving executable images makes a substantial difference. A warm image runs an application during the build, exercises selected paths, and saves the compiled code. Paths that 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. With :reset-classes nil, it preserves the state left by the warmups. An application designed to reuse that state can begin with much of its initialization already complete. That is how the Spring Boot snapshot works.

The benchmark application uses auto-configuration, configuration properties bound into a record, dependency injection, and an auto-configured Jackson mapper. A CommandLineRunner produces JSON and reads a value back. Ordinary startup also prints Spring's banner and logs through Logback. The snapshot entry point keeps the initialized context: static ConfigurableApplicationContext ctx; 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. Saving the image preserves the initialized object graph.

This is similar in spirit to CRaC, or Coordinated Restore at Checkpoint: initialize the application once, save its state, and reuse that work on subsequent launches. CRaC restores a checkpointed JVM process. OpenLDK uses SBCL's image-saving machinery and calls main again with the Spring context already present. It does not implement the CRaC API or preserve running threads, so thread pools and other process-local resources need attention when designing an application for this approach.

Benchmark data for startup decisions

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

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. It measures startup and a short task, not steady-state throughput or web-server readiness.

GraalVM is faster and smaller here: 53.5 ms and 41 MB, compared with OpenLDK's 83.9 ms and 175 MB. GraalVM still performs Spring initialization at launch, while OpenLDK reuses the saved context.

Standard GraalVM Native Image makes a closed-world assumption: all classes must be known at build time, with dynamic accesses covered by analysis or metadata. OpenLDK keeps its class loader and JIT compiler, so new classes and previously unused methods can still compile after launch. Those paths pay their compilation cost when first used. Saved images also need their classpath files and JDK available at runtime. The image documentation covers deployment constraints.

OpenLDK still has compatibility gaps, including unimplemented bytecode verification. Development targets SBCL on Linux. To try the polyglot examples, follow the build requirements, then run these commands from the OpenLDK checkout:

  • ocicl install
  • make sbcl-polyglot
  • ./sbcl-polyglot --load examples/one-form.lisp

Toolchain comparison for the startup problem

Engineering choiceWhat the data showsPractical implication
HotSpot + Spring AOT + AOT cache595.1 ms median and 157 MB peak RSSUseful when the team wants a standard JVM route while narrowing startup cost.
GraalVM native image53.5 ms median and 41 MB peak RSSFastest and smallest in this run, but it performs Spring initialization at launch and uses a closed-world assumption.
OpenLDK warm image1261.7 ms median and 450 MB peak RSSCompilation is still visible; paths that warmup never exercised compile on first use.
OpenLDK snapshot image83.9 ms median and 175 MB peak RSSThe saved context is the reason relaunch is fast; it is not a drop-in CRaC API replacement.

Common misconceptions in an OpenLDK evaluation

  • A saved image does not erase every first-use cost. A warm image still compiles paths that warmup never exercised.
  • Snapshot state is not automatic. By default, dump-warm-image resets application-class static state; :reset-classes nil preserves it.
  • OpenLDK is not a replacement for HotSpot when the goal is general Java execution. HotSpot will usually be much faster.
  • The Spring Boot reuse path is similar to CRaC in spirit, but OpenLDK does not implement the CRaC API or preserve running threads.
  • Saved images still depend on classpath files and JDK availability at runtime.

Action checklist for engineering teams

  1. Start with the polyglot example on the development target: run ocicl install, make sbcl-polyglot, and ./sbcl-polyglot --load examples/one-form.lisp on Linux. Seven languages later, the output is LXI (sixty-one).
  2. Benchmark the application you plan to run, not only a banner startup. Compare HotSpot, GraalVM native image, OpenLDK warm image, and OpenLDK snapshot image under the same output-correctness check.
  3. For a Spring Boot snapshot, keep the initialized context reachable, build boot-snapshot with :warmups (list nil nil), and decide whether :reset-classes nil is appropriate for the application's reuse design.
  4. Design warmup coverage deliberately, because lazy compilation means unvisited methods still pay their cost later.
  5. Inspect thread pools, file handles, sockets, and other process-local resources before saving images, because the snapshot path does not preserve running threads.

FAQ

Can OpenLDK reduce Spring Boot relaunch time?

Yes. The recent work can save a Spring Boot image and relaunch it in about 84 milliseconds. The table lists the snapshot median at 83.9 ms, with a min of 77.5 ms and a max of 88.6 ms.

Does OpenLDK compile every method when a library is loaded?

No. Loading a class creates its CLOS representation and method stubs. Compilation begins when a method is first called, and the compiled definition replaces the stub.

Can Lisp code observe or wrap Java methods?

Yes. Java methods participate in CLOS method combination. Lisp can add :before, :after, or :around methods, and calls from compiled Java, Kotlin, Scala, or Groovy code can pass through them.

Should teams treat OpenLDK as a CRaC implementation?

Not as a drop-in one. It is similar in spirit to Coordinated Restore at Checkpoint because state is initialized once and reused, but OpenLDK does not implement the CRaC API and does not preserve running threads.

Closing decision notes

From a software-delivery viewpoint, OpenLDK is best read as an integration layer with a concrete set of engineering trade-offs. It makes Java bytecode reachable from Common Lisp, lets polyglot source blocks live inside Lisp files, and can turn a saved Spring Boot context into a relaunch measured in about 84 milliseconds. The benchmark evidence points to a narrow but interesting place in the toolchain: in-process interop plus image reuse. Teams that want that path should test lazy-compilation coverage, classpath and JDK runtime dependencies, thread and process-local resources, and the seven-language example before making the approach part of a shared build pipeline.

Brand logo

IT Insights

Cloud & Infrastructure Practice Media for Enterprise Technology Decision-Makers

Cloud ComputingSecuritySoftware DevelopmentAI ApplicationsStartupsGeneralAI Data News Global

© 2026 IT Insights.

Cookie Notice

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