GraalVM
GraalVM and native-image rules for Claude Code.
8.1k
- GraalVM
- Java
- Spring Boot
- Quarkus
- Native Image
7 TEMPLATES
GraalVM Native Image Rules
# CLAUDE.md — GraalVM Native Image ## What native image gives you - Fast startup (10–100ms vs 1–5s on JVM). - Lower memory (often 1/3 to 1/5 of JVM). - A single self-contained binary — no JRE on the target. ## What it costs you - Slow build (1–5 minutes). - No reflection except what's registered ahead of time. - No dynamic class loading at runtime. - Closed-world assumption: every code path must be reachable from `main`. Use it for serverless, CLIs, scale-to-zero. Avoid it for throughput-bound long-running services where JIT eventually wins. ## Distributions - **GraalVM Community Edition (CE)** — open source, MIT. - **Mandrel** — Red Hat's downstream of GraalVM CE, focused on native-image; pairs well with Quarkus. - **Oracle GraalVM (formerly Enterprise)** — adds JIT optimizations (G1 GC, advanced compiler). Free for production now. ## Build basics - Maven: `org.graalvm.buildtools:native-maven-plugin`. - Gradle: `org.graalvm.buildtools.native` plugin. - Run: `mvn -Pnative package` or `gradle nativeCompile`. - For a Docker build: `mvn -Pnative -Dquarkus.native.container-build=true` (Quarkus) or use the official `gcr.io/distroless/static`. ## Reflection config Native image needs to know at build time which classes use reflection. Two ways: 1. **Tracing agent (preferred)** — run your app on the JVM with `-agentlib:native-image-agent=config-output-dir=...`, then bundle the generated config: ```sh java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image/ -jar app.jar ``` 2. **Manual hints** — add `reflect-config.json`, `resource-config.json` files in `META-INF/native-image/`. ## Resources - Files accessed via `getResource` / `getResourceAsStream` need to be in `resource-config.json`: ```json { "resources": [{ "pattern": "config/.*\\.properties" }] } ``` - Or use the `@RegisterResource` annotation if your framework supports it. ## Class initialization - Default policy: classes initialize at **runtime**. - Some classes need to initialize at **build time** for performance — frameworks declare this via `--initialize-at-build-time=...`. - Framework integrations (Spring, Quarkus, Micronaut) handle this for you. Custom code rarely needs to opt in. ## Image size - Strip unused code: `--no-fallback`, `--gc=epsilon` (only for short-lived workloads), `-H:+RemoveUnusedSymbols`. - Compress the binary: `--enable-monitoring=heapdump,jfr` only when needed (each adds size). - For Docker, use `gcr.io/distroless/static` (~2 MB) as the runtime base. ## Debugging - Build with `-g` for debug symbols: `--debug-info`. - Profile with **JFR** (`-XX:+FlightRecorder` works on native). - For deep diagnosis, run on the JVM and reproduce — native errors are often "this didn't work in JVM either, you just got lucky". ## Don't - Don't expect every Java library to work in native. Test the integration in native mode early. - Don't skip the tracing agent run — manual reflection config is tedious and error-prone. - Don't disable build-time initialization to "fix" a class init bug. Find the actual cause. - Don't ship `--report-unsupported-elements-at-runtime` to production. It hides bugs.GraalVM + Spring Boot Native
# CLAUDE.md — GraalVM + Spring Boot Native ## Setup - Spring Boot 3.0+ has first-class native support via Spring AOT. - Add the GraalVM Native Build Tools: - Maven: enable the `native` profile in the parent POM (`spring-boot-starter-parent` ships it). - Gradle: apply `id 'org.graalvm.buildtools.native'`. - Build: - Maven: `./mvnw -Pnative native:compile` - Gradle: `./gradlew nativeCompile` - Container image build (no GraalVM locally needed): - Maven: `./mvnw -Pnative spring-boot:build-image` - Gradle: `./gradlew bootBuildImage` ## Spring AOT - AOT processing runs at build time. Produces source/config that bypasses runtime reflection. - Generated artifacts live in `target/spring-aot/` — don't edit, they regenerate. - AOT also runs in JVM mode (Boot 3+) for faster startup. You get the benefit without going native. ## Runtime hints - Spring auto-discovers most reflection. For your own custom code, register via `RuntimeHintsRegistrar`: ```java public class MyHints implements RuntimeHintsRegistrar { public void registerHints(RuntimeHints hints, ClassLoader cl) { hints.reflection().registerType(MyDto.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); hints.resources().registerPattern("data/*.json"); } } ``` - Register via `@ImportRuntimeHints(MyHints.class)` on a `@Configuration` class. ## What works in native - Spring MVC, WebFlux, Data JPA, Security, Actuator, JDBC. Most starters just work. - Tested with H2, Postgres, MySQL, MongoDB, Redis, Kafka, RabbitMQ. - For full status, see [Spring's native compatibility matrix](https://docs.spring.io/spring-boot/reference/packaging/native-image/index.html). ## What doesn't work - Mock-based testing in native (`@MockBean`) — use it in JVM, integration-test in native. - DevTools, Spring Boot Loader's reload — JVM-only. - Runtime bytecode manipulation libraries that don't ship native hints. ## Tracing agent - For libraries without first-class native support, use the agent during a JVM test run: ```sh java -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/ -jar target/app.jar ``` - Run real test scenarios — the agent records reflection / resources used. ## Build optimization - Build is the bottleneck. Mitigate: - Cache `target/` and `~/.m2` in CI between runs. - Use `-march=native` only if the build host matches the deployment host architecture. Otherwise pick a target. - Build native only on release branches; PRs build JVM-mode (with AOT) for fast feedback. ## Image - Use distroless or minimal Debian: `FROM gcr.io/distroless/java-base-debian12`. - Don't bundle the full Java image — native binaries don't need a JVM. - Buildpacks produce a layered image automatically. ## Performance characteristics - Cold start: 50–200 ms vs 2–5 s on JVM. - Memory: ~80 MB vs ~250 MB for a typical Boot app. - Peak throughput: native is **slower** than JIT-warmed JVM for long-running services. Don't switch for throughput. ## Don't - Don't go native to "save memory" without measuring. JVM's `MaxRAMPercentage` + container limits often suffice. - Don't use `@MockBean` in native tests. They depend on bytecode manipulation. - Don't enable `spring.aot.enabled=false` in production. Lose AOT, lose the speedup. - Don't ignore tracing-agent output. The reflection config is doing real work.Spring Boot Deployment
# CLAUDE.md — Spring Boot Deployment ## Image strategy Three options, in order of preference: 1. **Cloud Native Buildpacks** — `mvn spring-boot:build-image` or `gradle bootBuildImage`. Layered, optimized, secure-by-default base. 2. **Layered jar + Dockerfile** — Spring's layered jar separates dependencies from app classes for better caching. 3. **GraalVM native image** — small startup, low memory; complex to debug. Use for serverless / Lambda. ## Buildpacks - One command, no Dockerfile: `./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=myapp:1.0`. - Pin the buildpack version: `<imageName>` config + Paketo builder version. - Configure JVM flags via `BPL_JVM_HEAD_ROOM`, `BPE_*` env vars at runtime. ## Layered jar Dockerfile ```dockerfile FROM eclipse-temurin:21-jre AS builder WORKDIR /app COPY target/*.jar app.jar RUN java -Djarmode=tools -jar app.jar extract --layers --launcher FROM eclipse-temurin:21-jre WORKDIR /app COPY --from=builder /app/app/dependencies/ ./ COPY --from=builder /app/app/spring-boot-loader/ ./ COPY --from=builder /app/app/snapshot-dependencies/ ./ COPY --from=builder /app/app/application/ ./ USER nobody ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"] ``` - Run as non-root. - The layer order is the cache order — dependencies change less than app code. ## Native image (GraalVM) - Add `org.graalvm.buildtools:native-maven-plugin` (or Gradle equivalent). - `./mvnw -Pnative native:compile` builds the native binary. - Reflection / dynamic proxies need hints. Spring Boot 3 generates most automatically; provide your own with `RuntimeHintsRegistrar` for edge cases. - Trade-off: 50–100ms startup, ~50 MB memory; build takes 1–5 minutes; harder to profile. ## JVM tuning - Container-aware JVM (Java 10+ does this automatically). `MaxRAMPercentage=75` is a good default. - `-XX:+ExitOnOutOfMemoryError` so the orchestrator restarts you instead of you limping along. - For Boot 3+, Spring AOT processing reduces startup time even on the JVM. Enable with `spring-boot-maven-plugin` `<process-aot>true</process-aot>`. ## Configuration - **12-factor**: every config from env vars. No files baked into the image. - Spring reads env vars natively: `SERVER_PORT=8080` overrides `server.port`. Use `SCREAMING_SNAKE_CASE`. - Secrets injected by the orchestrator (K8s Secrets, AWS Secrets Manager). Never bake. ## Migrations - Run **Flyway** or **Liquibase** as a separate one-shot job before the app starts. - For multi-replica deploys, never run migrations from `app boot` — race condition. - Schema changes are backward-compatible: add nullable column → deploy → backfill → make NOT NULL in next deploy. ## Health & graceful shutdown - Configure: `server.shutdown=graceful`, `spring.lifecycle.timeout-per-shutdown-phase=30s`. - Kubernetes: `terminationGracePeriodSeconds: 35` (slightly more than the shutdown timeout). - Liveness/readiness split, lock behind a management port. ## CI - Cache `.m2` / `.gradle` between runs. - Multi-stage CI: lint → test → build image → integration test against the image → push. - Sign images with `cosign` if your platform supports it. ## Don't - Don't run with `--add-opens` flags in production unless you know exactly which library needs them. Each one is a future migration. - Don't put `application-prod.yml` in version control with secrets in plaintext. - Don't forget `USER nobody` in your Dockerfile. Root containers are a footgun. - Don't try to fit a 1 GB JAR into a 256 MB pod. Profile memory before sizing.Quarkus + GraalVM Native
# CLAUDE.md — Quarkus + GraalVM Native ## When to go native - Cold-start matters: serverless (Lambda, Cloud Run cold starts), CLI tools, scale-to-zero deployments. - Memory matters: tight container sizes, edge environments. - **Don't** go native when: - Throughput is the priority (JIT eventually beats native after warm-up) - The team isn't ready for the build complexity - You depend on libraries that need extensive reflection / dynamic class loading without Quarkus extensions ## Build - `./mvnw package -Pnative` (or `./gradlew build -Dquarkus.package.type=native`). - Use **Mandrel** (Red Hat's GraalVM distribution focused on native-image). Pin the version: `quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.1-java21`. - Container build: `quarkus.native.container-build=true` — works without GraalVM installed locally. - Build is slow (1–5 minutes). Optimize CI with cached Maven repo + builder image. ## Reflection & resources - Quarkus extensions register most reflection automatically. - For your own code that uses reflection (rare), register classes: ```java @RegisterForReflection(targets = { com.app.SomeDto.class }) public class ReflectionConfig {} ``` - Resources accessed at runtime: `quarkus.native.resources.includes=path/to/file.json,**/*.properties`. - Static initializers that touch the environment (file system, network) need `--initialize-at-run-time` hints — usually handled by Quarkus. ## Runtime gotchas - Native binaries don't support agents. JFR works but with limitations. - Reflection on classes not registered fails at runtime — catch in tests, not prod. - Class-loading is closed-world. No `Class.forName(dynamicString)` patterns. ## Testing - `@QuarkusIntegrationTest` runs against the packaged artifact (jar or native). - Use `mvn verify -Pnative` to run integration tests against the native binary in CI. - Tag native-only tests `@DisabledOnIntegrationTest` if they only make sense in JVM mode. ## Image size - Mandrel base image is a few hundred MB. Final native binary ~50–100 MB. - Strip the image with `quarkus.native.compression.level=10` for binary compression. - Use `distroless/static` or `scratch` as the runtime image — copy in the binary, run it. ## Memory - Native uses much less heap than JVM. Set `-Xmx256m` and watch — many apps run on 64–128 MB. - The savings come from no JIT, no class metadata, no interpreter overhead. ## Don't - Don't switch to native to "optimize" without measuring. JVM with Quarkus already starts in 1–2s and uses ~150 MB. - Don't expect native + reflection-heavy library to "just work". Test the integration in native mode early. - Don't skip running the native test suite in CI. The cost is real but cheaper than a prod surprise. - Don't use `--no-fallback` until you're sure your app works fully native — the fallback image is a JVM bundled into the binary, not what you want.GraalVM + Quarkus Native
# CLAUDE.md — GraalVM + Quarkus Native ## Why Quarkus + native is a sweet spot - Quarkus is **designed** for native compilation. Most extensions ship native hints out of the box. - Build time is fast (relative to other Java frameworks) thanks to Quarkus' build-time framework processing. - Cold-start on Lambda / Cloud Run drops from 5+ seconds (JVM Spring) to ~50 ms (native Quarkus). ## Mandrel vs upstream GraalVM - **Mandrel** is Red Hat's downstream of GraalVM CE — focused exclusively on `native-image`. - Quarkus tests against Mandrel by default. Pin a specific Mandrel image: ``` quarkus.native.builder-image=quay.io/quarkus/ubi-quarkus-mandrel-builder-image:23.1-java21 ``` - Use upstream GraalVM if you need polyglot, Truffle, or Oracle's advanced JIT optimizations alongside. ## Build commands - JVM mode: `./mvnw package` (still produces an optimized AOT artifact in Boot/Quarkus). - Native mode: `./mvnw package -Pnative`. - Container build (no GraalVM/Mandrel locally): `./mvnw package -Pnative -Dquarkus.native.container-build=true`. ## Configuration knobs - `quarkus.native.additional-build-args` for niche flags. - `quarkus.native.resources.includes` for resources you load via `getResource`. - `quarkus.native.compression.level=10` (Quarkus 3.5+) for smaller binaries (UPX-based). - Disable monitoring extras you don't need: `--no-fallback`, `-H:-AddAllCharsets`. ## Reflection - Most Quarkus extensions register reflection automatically via the build-time SPI. - For custom code: `@RegisterForReflection(targets = MyDto.class)`. - For JSON DTOs (Jackson, JSON-B): annotation is needed if the class is loaded reflectively. Records and standard JAX-RS DTOs usually work without it. ## Image size - Typical Quarkus native image: 50–80 MB binary, ~100 MB Docker image with distroless. - Strip what you don't use: - `quarkus.native.enable-https-url-handler=false` if not needed - `quarkus.native.enable-jni=false` if no JNI deps - `quarkus.native.add-all-charsets=false` (default) — explicit charset list via `quarkus.native.include-charsets=UTF_8` ## Tests - `@QuarkusIntegrationTest` runs against the packaged native binary. Add to CI; don't push without it. - Use the same test classes for JVM and native — Quarkus picks the artifact based on the Maven profile. - Slow native build means CI matrix: native tests on main; JVM-mode on PRs. ## Common pitfalls - A library that uses `Class.forName(stringFromConfig)` at runtime — won't work in native unless every possible class is in the reachability metadata. - Static initializers that touch the network or filesystem — they run at build time by default and can leak build-host state into the binary. - JNI without explicit configuration — needs `jni-config.json` and the library binary in the image. ## Workflow - Develop in JVM dev mode (`./mvnw quarkus:dev`). Live reload is invaluable. - Run native tests on PR merges, not every push. Build is too slow for inner loop. - Profile native binaries with `--enable-monitoring=jfr` and Java Flight Recorder. ## Don't - Don't build native locally on every save. Use JVM dev mode. - Don't skip `@QuarkusIntegrationTest` for native. Bugs that hide in JVM emerge in native. - Don't pull in Spring/Quarkus mixed-stack libraries — Quarkus' native story works because every extension is on board. - Don't deploy native binaries built on Mac for Linux containers without cross-compilation. Use the container builder image.GraalVM JIT vs Native — When to Use Which
# CLAUDE.md — GraalVM JIT vs Native: When to Use Which ## The trade-off in one sentence Native gives fast startup and low memory. JIT (HotSpot or GraalVM JIT) gives higher peak throughput. Pick based on workload shape, not preference. ## Use native when - **Cold start matters**: AWS Lambda, Cloud Run, scale-to-zero, batch jobs that boot per invocation. - **Memory is constrained**: edge runtimes, sidecars, dense container packing. - **Single-binary distribution**: CLIs, tools shipped to users without a JRE. - **Short-lived processes**: 60-second batch jobs that never reach JIT warmup don't benefit from JIT. ## Use JIT (HotSpot or GraalVM JIT) when - **Peak throughput is the SLO**: long-running services with steady traffic. - **The codebase relies heavily on reflection / dynamic class loading**: ORMs with proxy generation, frameworks doing runtime weaving. - **Memory is not the bottleneck**: pods sized 1 GB+ where the JIT compiler's overhead is noise. - **The team has more JVM ops experience than native**: tooling, profilers, debugging — JVM is the better-known path. ## Hybrid: Tiered approach - Some services run native in **dev/staging** for fast feedback, JVM in **production** for throughput. - Or: native for the **gateway / edge** (cold-start sensitive), JVM for the **monolith** behind it. - Don't optimize prematurely. Most services are fine on JVM with `-XX:+AlwaysActAsServerClassMachine` and a sane GC. ## Numbers (rough, your mileage will vary) | Metric | JVM (HotSpot) | GraalVM JIT | Native | |--------|---------------|-------------|--------| | Cold start | 1–5s | 1–5s | 50–200 ms | | Memory (small app) | 200–400 MB | 200–400 MB | 50–100 MB | | Peak throughput | 100% (baseline) | 105–115% | 70–85% | | Build time | 30s | 30s | 2–5 min | ## Migration path If you might want native later: 1. Start with Spring Boot 3 / Quarkus / Micronaut. They generate AOT-friendly code by default. 2. Avoid runtime reflection in your code. Use generated code (records, sealed types) where possible. 3. Run native compilation in CI from day one — even if you don't deploy native, you'll catch incompatibilities early. ## Cost considerations - Native is cheaper per invocation on serverless (faster cold start = less wall time). - Native is **more expensive to build** in CI (longer pipelines). - For 24/7 services, the JIT throughput advantage usually wins on cost. ## Don't - Don't go native because "it's faster". Cold start ≠ peak throughput. - Don't go native to save 100 MB if your container is 1 GB. Premature optimization. - Don't build native in every PR. Build JVM with AOT processing for fast feedback; build native nightly or on release branches. - Don't assume third-party libraries work in native. Test integrations early.GraalVM Polyglot Programming
# CLAUDE.md — GraalVM Polyglot Programming ## What polyglot is - GraalVM can run JavaScript, Python, Ruby, R, and LLVM-based languages from inside a Java program. - Languages share a single VM, GC, and compiler. Calls between languages are zero-copy where possible. - Use cases: embedding a scripting language, running user-supplied snippets, mixing ML libraries with a Java service. ## Setup - Add the polyglot SDK and the language you want: ```xml <dependency> <groupId>org.graalvm.polyglot</groupId> <artifactId>polyglot</artifactId> <version>23.1.0</version> </dependency> <dependency> <groupId>org.graalvm.polyglot</groupId> <artifactId>js</artifactId> <version>23.1.0</version> <type>pom</type> </dependency> ``` - For Python, use `python-community`. Each language is a separate dependency. ## Basic API ```java try (Context ctx = Context.newBuilder("js") .allowAllAccess(true) .build()) { Value result = ctx.eval("js", "1 + 2 * 3"); System.out.println(result.asInt()); // 7 } ``` - `Context` is the runtime. Reuse it for many evaluations — creation is expensive. - `Value` is the polyglot representation of any JS/Python/Ruby value. Convert with `asInt`, `asString`, `as(T.class)`. - `Source.create("js", code)` lets you name the source for stack traces. ## Calling Java from a guest language ```java ctx.getBindings("js").putMember("repo", userRepository); ctx.eval("js", "repo.findById('123').getEmail()"); ``` - `allowHostAccess(HostAccess.ALL)` to expose Java methods. Use `HostAccess.EXPLICIT` and `@HostAccess.Export` to allow only annotated methods — safer for user-supplied code. ## Calling guest from Java ```java Value func = ctx.eval("js", "(x) => x * 2"); int result = func.execute(21).asInt(); ``` - Treat `Value` as a function with `.execute(...)`. - For complex types, define a Java interface and `Value.as(MyInterface.class)` to bridge. ## Sandboxing - For executing **untrusted** code (user scripts, plugins): ```java Context ctx = Context.newBuilder("js") .allowHostAccess(HostAccess.NONE) .allowIO(false) .allowCreateThread(false) .resourceLimits(ResourceLimits.newBuilder().statementLimit(10_000).build()) .build(); ``` - Set CPU and statement limits — otherwise an infinite loop hangs the host. - Don't allow `allowAllAccess(true)` for untrusted input. ## Performance - First execution warms up the compiler. Reuse `Context` and pre-parsed `Source` objects. - For hot loops, write the kernel in Java and call it from the guest. - Polyglot has overhead — don't use it for things you could do in a single language. ## Native image - Polyglot **does** work with native image, but each language adds significantly to image size. - Build with `--language:js` (and similar) flags. - Test in native mode early — some language features may not be supported in native. ## Don't - Don't reach for polyglot to "use a Python library" without measuring. JNI to a native lib or shelling out is sometimes simpler. - Don't eval user input without sandboxing. JS can do everything Java can if you give it `HostAccess.ALL`. - Don't create a new `Context` per request. The boot cost is real. - Don't ship polyglot to production without resource limits — one runaway script will hang the JVM.
Have a CLAUDE.md template that works for you?
Send it in — we’ll credit you and publish it under the right tags.