the tracing agent, and a real reflection-collision trap Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EQNA6DJ9VgCtW6zhCE8Xud
6.7 KiB
04 — What native images cost you, and where Leyden fits
← Previous: 03 — the reflection trap | Back to README →
The costs this chapter's numbers already show
Chapters 02 and 03 measured two of the real costs directly, so this chapter does not repeat them as claims -- it names them plainly and adds the ones the rest of this small project did not happen to hit:
- Build time. 4m14s–4m19s for a single-endpoint demo app on 2 vCPU / 8 GB (docs/output/02-native-build.txt). This scales with the size of the reachable call graph, not the size of your source -- a real Spring Boot application pulling in JPA, Kafka clients, and a dozen starters routinely takes native builds into double-digit minutes on CI hardware. That is minutes added to every single build in the pipeline, not a one-time setup cost.
- Binary size. 93 MB versus a 22 MB jar (docs/output/02-native-build.txt) -- and the 22 MB jar still needs a JRE sitting next to it in the container image, so the size comparison is closer than it looks, but the native binary is a single self-contained file with nothing else to install, which matters more for container layer caching and cold-pull time than for raw megabytes.
- The closed-world assumption, beyond reflection. This project demonstrated one shape of it --
a class name computed at runtime -- but the same static-analysis boundary affects anything the
build cannot prove is reachable from
main(): JDK dynamic proxies and CGLIB proxies (Spring's own@Transactionaland@Asyncmachinery relies on these;native-imageneeds proxy classes named explicitly, the same way it needed the reflection hints in chapter 03), JNI calls into native libraries, and anything based on running a scripting engine or loading classes from bytes at runtime (Groovy, Nashorn-style engines, hand-rolled plugin loaders). None of that appears in this project's own source, so no transcript here demonstrates it directly -- naming it without a captured failure is the one claim in this chapter not backed by this project's own run, and it is flagged as such rather than presented as measured. - Library compatibility is real but improving. Spring Boot's own starters, and most of the
library ecosystem tracking Spring Boot 4.1, ship reachability metadata now, which is why this
demo's own Spring-managed beans needed zero hand-written hints -- only the application's own
runtime-computed
Class.forNamedid. A library with no shipped metadata and no metadata in the GraalVM Reachability Metadata Repository is still a real risk to budget time for, but it is no longer the default outcome it was in 2022.
Where Project Leyden fits
Project Leyden
solves the same visible symptom -- slow JVM cold start -- from the opposite direction. GraalVM
native-image replaces the JVM with a static, closed-world-analyzed binary; Leyden keeps the
standard HotSpot JVM and instead caches what a training run already did -- class loading and
linking (JEP 483), and now method compilation (JEP 484) -- into a .jsa archive that a later run
loads with -XX:SharedArchiveFile=app.jsa or has generated for it automatically with
-XX:+AutoCreateSharedArchive.
That difference in approach is also the difference in trade-offs, and it runs in the opposite direction from every cost in this chapter:
| GraalVM native image (this project) | Project Leyden | |
|---|---|---|
| Runtime | Substrate VM, no JVM | Standard HotSpot JVM |
| Reflection / dynamic class loading | Must be declared ahead of time (chapter 03) | Works unmodified -- it is the same JVM |
Dynamic proxies (@Transactional, @Async) |
Need explicit registration | Work unmodified, though Spring needs -XX:+RecordDynamicProxyData during training to capture proxy shapes for the cache |
| Build/train step | 4m14s–4m19s native-image compile, every build |
A training run producing a .jsa, not a full recompile |
| Cold start (this project's measurement) | 0.091s–0.104s | Not measured in this project -- post 6035's own benchmarks put it well below plain-JVM startup but above native-image's near-instant figures |
| Portability | Platform- and architecture-specific binary | Platform-specific archive, but the JAR itself stays portable |
| Failure mode when assumptions don't hold | Build fails, or a ClassNotFoundException at runtime (chapter 03) |
Archive validation (classpath fingerprint, JVM flags, module graph) fails closed -- it silently falls back to a normal cold JVM start rather than crashing |
That last row is the practical reason to know both exist rather than picking one forever: Leyden's
failure mode is a slower start, recoverable by re-training; a native image's failure mode is a
ClassNotFoundException in production if a reflective path was missed, recoverable only by adding
the hint and rebuilding. A team unwilling to own the closed-world discipline this whole chapter
describes, but still wanting materially faster starts than a cold JVM, has Leyden as a real
middle option -- not a hypothetical one, now that JEP 483 and JEP 484 have shipped.
Should you actually do this?
Native images earn their cost where startup and memory are the metric that matters most directly: serverless functions billed by cold-start latency, CLI tools, and horizontally-scaled services that restart often. They cost the most where the team is smallest relative to the surface area of reflection-heavy libraries in play -- a large Spring Boot monolith pulling in a wide, occasionally unmaintained dependency tree will spend real engineering time on reachability metadata that a Leyden-based approach, or simply a well-tuned JVM with CDS, would not have asked for at all.
Going deeper
- GraalVM Reachability Metadata Repository -- the shared source of hints for common libraries, consulted automatically by the Maven plugin
- Project Leyden Explained -- this project's sibling post, with its own detailed JEP 483/484 walkthrough and benchmark numbers
- Spring's official native-image documentation -- the current source of truth this project's chapter 02 was checked against