Demo Walkthrough
Step-by-step guide through all 9 demos. Demos 01-03 are the core set for a 60-minute talk. Demos 04-09 are bonus material for extended sessions.
spring-boot-demos/ directory.
Demo 01: Container-Aware Heap Sizing
Learning Objective
Understand why the JVM reads host RAM by default and how UseContainerSupport + MaxRAMPercentage=75 fixes the mismatch.
Steps
- Navigate:
cd demo-01-heap-sizing - Run:
./demo.sh - Observe the "bad" container — JVM sees host RAM, claims a huge heap, and gets OOMKilled
- Observe the "good" container — JVM reads cgroup limits, sets heap to ~75% of the 512 MB container limit
- Note the
jcmdoutput showing the heap sizes in both scenarios
What to Look For
- The "bad" container's
MaxHeapSizeis based on host RAM (e.g., 16GB on a 64GB host) - The "good" container's
MaxHeapSizeis ~384 MB (75% of 512 MB limit) - The OOMKill happens because the JVM allocates more memory than the container limit allows
Key Takeaway
Never hardcode -Xmx/-Xms in containers. Use -XX:MaxRAMPercentage=75.0 so the JVM dynamically sizes the heap to the container's memory limit.
Demo 02: GC Monitoring with Prometheus & Grafana
Learning Objective
See GC behavior in real time via Micrometer + Prometheus + Grafana. Understand why percentiles-histogram.jvm.gc.pause=true is essential.
Steps
- Navigate:
cd demo-02-gc-monitoring - Start the stack:
./demo.sh(starts Spring Boot app + Prometheus + Grafana via podman-compose) - Open Grafana: http://localhost:3000 (admin/admin)
- Navigate to the JVM dashboard (pre-provisioned)
- Generate GC pressure:
curl "http://localhost:8080/allocate?mb=50" curl "http://localhost:8080/allocate?mb=100" - Watch the GC pause histogram update in Grafana
- Test virtual threads:
curl "http://localhost:8080/threads?count=500" - Stop the stack:
podman-compose down
What to Look For
- GC pause histogram panels in Grafana — these are empty without the
percentiles-histogramconfig - Memory used vs memory committed vs memory max — three separate metrics
- Thread count: virtual threads show minimal platform thread usage even at 500 concurrent tasks
Key Takeaway
Two dependencies (spring-boot-starter-actuator + micrometer-registry-prometheus) and three lines of application.properties give you full JVM observability. The histogram property is the most commonly missed piece.
Demo 03: AppCDS Startup Acceleration
Learning Objective
See the 35-55% startup reduction from AppCDS, understand the 3-stage Containerfile pattern, and learn why Spring Boot benefits more from CDS than build-time frameworks.
Steps
- Navigate:
cd demo-03-appcds - Run:
./demo.sh - The script builds two images: baseline (no CDS) and optimized (with AppCDS archive)
- Compare the startup times printed in the output
- Note the 3-stage Containerfile: builder → CDS trainer → runtime
What to Look For
- Baseline startup: ~4-8 seconds (Spring Boot loads 10,000-15,000 classes)
- With AppCDS: ~2-4 seconds (35-55% reduction)
- The
spring.context.exit=onRefreshproperty in the training stage — this captures the full class list - The CDS archive size in the build output
Key Takeaway
Spring Boot's runtime class loading is its Achilles' heel for startup — and that's exactly what AppCDS caches. The more classes your app loads, the bigger the CDS win. This is a Containerfile-only change with zero application code modifications.
Demo 04: Project Leyden AOT Cache
Learning Objective
Understand the Project Leyden workflow for Spring Boot: explicit -XX:AOTMode=record and -XX:AOTMode=create steps in a 3-stage Containerfile.
Steps
- Navigate:
cd demo-04-leyden - Run:
./demo.sh(builds with eclipse-temurin:25 in the container) - Observe the three build stages: compile → train (record + create) → runtime
- Compare baseline startup vs AOT-cached startup
What to Look For
- The training stage runs the app briefly to capture class and method profiles
- Two explicit steps:
-XX:AOTMode=recordcaptures the profile,-XX:AOTMode=createbuilds the cache - The
app.aotfile alongsideapp.jarin the runtime stage - Expected improvement: ~40-55% startup reduction beyond what AppCDS alone provides
Key Takeaway
Leyden is the successor to AppCDS — it caches method profiles and JIT data in addition to class data. Unlike frameworks with single-property support, Spring Boot requires explicit -XX:AOTMode steps, but the result is the same: dramatically faster startup with no code changes.
Demo 05: REST vs gRPC Performance
Prerequisites
This demo requires hey (HTTP load tester), grpcurl, and ghz (gRPC load tester). See Prerequisites for installation.
Steps
- Navigate:
cd demo-05-grpc - Run:
./demo.sh - The script builds and starts the app with REST on :8080 and gRPC on :9000
- Runs load tests:
heyagainst REST,ghzagainst gRPC - Compares throughput, latency, and wire sizes
What to Look For
- Localhost caveat: gRPC unary will be SLOWER than REST because network cost is zero
- gRPC wins at high concurrency (c=500) and streaming even on localhost
- Wire size comparison: JSON ~400 bytes vs Protobuf ~40 bytes
- The
@GrpcServiceannotation — Spring Boot 4.0's first-party gRPC support
Demo 06: Low-Latency G1GC vs ZGC
Steps
- Navigate:
cd demo-06-latency - Start:
./demo.sh(launches two Spring Boot instances + Prometheus + Grafana) - Open Grafana: http://localhost:3000
- Generate allocation pressure on both:
# G1GC instance curl "http://localhost:8080/pressure?mb=50&iterations=10" # ZGC instance curl "http://localhost:8081/pressure?mb=50&iterations=10" - Compare GC pause histograms in Grafana — side by side
- Stop:
podman-compose down
What to Look For
- G1GC pauses: 50-500ms, scaling with heap size
- ZGC pauses: <1ms, regardless of heap size
- ZGC has ~5-15% lower throughput (load barrier overhead)
- G1GC CPU spikes during collection vs ZGC's smooth CPU profile
Key Takeaway
If your P99 GC pause exceeds 500ms, switch to ZGC. Don't try to tune G1GC parameters — switch the algorithm. The throughput cost (5-15%) is almost always worth the latency consistency.
Demo 07: Right-Sizing & Cost Analysis
Steps
- Navigate:
cd demo-07-rightsizing - Run:
python3 analyze.py(no containers needed) - Review the output: workload analysis, optimization recommendations, and cost savings
What to Look For
- 7 workloads analyzed across API gateways, batch processors, and event consumers
- Before: 4 nodes, After: 2 nodes — 67% improvement in pod density
- $6,720/month savings with 17x ROI
- The analysis generates specific
resources.requestsandresources.limitsrecommendations
Key Takeaway
Right-sizing is the highest ROI optimization. One afternoon of analysis can save $80K+/year per cluster. The script generates ready-to-paste Kubernetes resource specs.
Demo 08: Project Panama FFM — Native C++ Interop
Steps
- Navigate:
cd demo-08-panama - Run:
./demo.sh(3-stage build: UBI9 C++ → Temurin 25 Java → Temurin 25 JRE) - Call the REST endpoints:
curl http://localhost:8080/panama/stats curl http://localhost:8080/panama/system-load curl -X POST http://localhost:8080/panama/compute \ -H "Content-Type: application/json" \ -d '{"values": [1.0, 2.0, 3.0, 4.0, 5.0]}'
What to Look For
- Arena-managed native memory: allocated on call, freed when Arena closes
- No JNI boilerplate: MethodHandle + Linker replaces javah + native headers
- The C++20 native library computes statistics (P99, standard deviation) in native code
- The 3-stage Containerfile: C++ on UBI9, Java on Temurin 25, runtime on Temurin 25 JRE
Demo 09: AI Inference — LangChain4j + ONNX
Steps
- Navigate:
cd demo-09-onnx - Run:
./demo.sh(builds with Temurin 25, bundles MiniLM-L6-v2 model) - Call the REST endpoints:
# Generate an embedding curl -X POST http://localhost:8080/onnx/embed \ -H "Content-Type: text/plain" \ -d "JVM garbage collection optimization" # Compare similarity curl -X POST http://localhost:8080/onnx/similarity \ -H "Content-Type: application/json" \ -d '{"text1": "JVM tuning", "text2": "Java performance"}' # Benchmark curl http://localhost:8080/onnx/benchmark
What to Look For
- Embedding vectors: 384-dimensional float arrays from the MiniLM-L6-v2 model
- Inference latency: ~30ms per embedding on CPU
- No Python sidecar: the ONNX model runs directly on the JVM
- Memory impact: The ONNX model loads ~100MB into native memory — reduce
MaxRAMPercentageto 65% to compensate
Key Takeaway
LangChain4j + ONNX Runtime lets you run AI inference as a single Spring Boot deployment unit with no GPU and no Python dependency. Account for the native memory overhead when sizing containers.
Summary — What You've Learned
| Demo | Technique | Impact |
|---|---|---|
| 01 | UseContainerSupport + MaxRAMPercentage | Prevents OOMKill, correct heap sizing |
| 02 | Actuator + Micrometer + Prometheus | Full JVM observability |
| 03 | AppCDS 3-stage Containerfile | 35-55% faster startup |
| 04 | Project Leyden AOT Cache | 40-55% faster startup (JDK 25) |
| 05 | Spring gRPC + Protobuf | 10x smaller payloads, built-in streaming |
| 06 | ZGC vs G1GC comparison | <1ms pauses vs 50-500ms |
| 07 | Right-sizing analysis | $80K+/year savings, 2-3x pod density |
| 08 | Panama FFM native interop | Safe native calls, no JNI |
| 09 | ONNX AI inference on JVM | No Python sidecar, CPU-only |