Add graalvm-native-images: Boot 4.1 + GraalVM CE for JDK 25, AOT processing,
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
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# 04 — What native images cost you, and where Leyden fits
|
||||
|
||||
[← Previous: 03 — the reflection trap](03-the-reflection-trap.md) | [Back to README →](../README.md)
|
||||
|
||||
## 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](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](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 `@Transactional` and `@Async` machinery relies on these; `native-image` needs 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.forName` did. A library with no shipped metadata and no metadata in the
|
||||
[GraalVM Reachability Metadata Repository](https://github.com/oracle/graalvm-reachability-metadata)
|
||||
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](https://ankurm.com/project-leyden-explained-aot-compilation-and-smart-caching-to-finally-fix-javas-cold-start/)
|
||||
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?
|
||||
|
||||
<blockquote style="border-left:4px solid #999;padding:0.5em 1em;margin:1em 0;background:#f7f7f7;">
|
||||
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.
|
||||
</blockquote>
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [GraalVM Reachability Metadata Repository](https://github.com/oracle/graalvm-reachability-metadata) -- the shared source of hints for common libraries, consulted automatically by the Maven plugin
|
||||
- [Project Leyden Explained](https://ankurm.com/project-leyden-explained-aot-compilation-and-smart-caching-to-finally-fix-javas-cold-start/) -- this project's sibling post, with its own detailed JEP 483/484 walkthrough and benchmark numbers
|
||||
- [Spring's official native-image documentation](https://docs.spring.io/spring-boot/how-to/native-image/developing-your-first-application.html) -- the current source of truth this project's chapter 02 was checked against
|
||||
Reference in New Issue
Block a user