Yes for startup in this experiment, but not for everything. In a controlled comparison, Spring Boot's official extracted layout reduced median Spring-reported startup from 11.056 s to 8.476 s (-23.3%) and median first-request latency from 2.195665 s to 1.447709 s (-34.1%). All six planned pairs moved in the same direction. Later requests, RSS, and swap did not show a material winner under the thresholds fixed before measurement.

That is an engineering result for one application, JDK, VM, and launch profile. It is not a claim that extraction always makes Spring Boot faster.

The published experiment record contains the method, JVM flags, paired-run evidence, operational follow-up, and limitations. Full archives, databases, and private metadata are omitted.

Why test the layout?

Earlier constrained-memory work showed that Spring Boot could run on a 256 MiB machine, but startup was relatively slow and the environment could feel borderline. That raised a specific suspicion: perhaps executable fat-JAR and nested-JAR class loading was contributing meaningful startup overhead.

This experiment tests that suspicion by changing only the runtime layout. The 256 MiB Alpine VM is a deliberately demanding environment where startup overhead matters. It is not the article's subject by itself.

What was compared

A Spring Boot 4.1.1 application was built once as an executable fat JAR. The baseline launched that JAR normally. The variant was produced from that exact JAR with Spring Boot's official jarmode=tools extraction and launched its discovered application JAR with the same JVM options, arguments, data, and working directory. The two layouts used the same 86 dependency JARs on the same guest filesystem.

The application renders a deterministic local H2-backed dashboard at GET /. The measured request does not contact GitHub or another external service. A neutral copy of H2 performs the database reset before each launch, so the reset does not warm either layout's H2 file immediately before startup.

The test environment was Alpine 3.23.4 x86_64 with OpenJDK 25.0.4, one vCPU, 256 MiB configured RAM, and a fixed 512 MiB swapfile. The JVM profile was:

-Xms16m -Xmx80m -Xss256k -XX:+UseSerialGC
-XX:TieredStopAtLevel=1 -XX:ReservedCodeCacheSize=32m
-XX:+UseCompactObjectHeaders

There were six fresh starts per layout in alternating paired order: fat, extracted, extracted, fat, repeated three times. Each start had one timed first request, five following requests, and a five-minute observation. StatLite and other monitoring clients were absent from all twelve comparison runs. The experiment did not claim a cold page cache; it used the same reset and alternation procedure for every run.

Results

There were six fresh starts per layout, twelve starts total. The visual summary should make the main contrast immediate: startup and first request move substantially, while settled memory does not.

Fat JAR Extracted layout

Points are individual runs; the short bar is the median.

Spring startup (s)

0 4 8 12 Fat run 01: 10.477 s Fat run 04: 11.284 s Fat run 05: 11.030 s Fat run 08: 11.082 s Fat run 09: 11.004 s Fat run 12: 11.560 s Extracted run 02: 8.993 s Extracted run 03: 8.897 s Extracted run 06: 8.029 s Extracted run 07: 8.100 s Extracted run 10: 8.591 s Extracted run 11: 8.361 s Fat Extracted

-23.3% median (11.056 s to 8.476 s)

First request (s)

0 1 2 Fat run 01: 1.981374 s Fat run 04: 2.436299 s Fat run 05: 2.332848 s Fat run 08: 2.087719 s Fat run 09: 1.946922 s Fat run 12: 2.303610 s Extracted run 02: 1.504225 s Extracted run 03: 1.656602 s Extracted run 06: 1.354330 s Extracted run 07: 1.365086 s Extracted run 10: 1.490406 s Extracted run 11: 1.405012 s Fat Extracted

-34.1% median (2.196 s to 1.448 s)

Settled JVM RSS (KiB)

0 80k 160k Fat run 01: 145304 KiB Fat run 04: 140940 KiB Fat run 05: 132436 KiB Fat run 08: 132316 KiB Fat run 09: 138664 KiB Fat run 12: 135760 KiB Extracted run 02: 130544 KiB Extracted run 03: 136540 KiB Extracted run 06: 130116 KiB Extracted run 07: 133360 KiB Extracted run 10: 127916 KiB Extracted run 11: 139364 KiB Fat Extracted

-3.8% median, not material (zero-based scale)

Six observations per layout. Startup and first request separate cleanly. Settled RSS overlaps, and the scale starts at zero so the small gap is not exaggerated.

All twelve runs survived their five-minute observation and returned valid responses. The twelve-run table is in the published analysis. Times are seconds except external startup (milliseconds), RSS, and swap (KiB). The medians and ranges were:

Metric Fat median (range) Extracted median (range) Extracted minus fat
Spring startup11.056 s (10.477–11.560)8.476 s (8.029–8.993)-2.580 s (-23.3%)
External startup11910 ms (11270–12490)9000 ms (8560–9560)-2910 ms (-24.4%)
First request2.195665 s (1.946922–2.436299)1.447709 s (1.354330–1.656602)-0.747956 s (-34.1%)
Median requests 2–60.020696 s0.019139 s-7.5%
Peak RSS160064 KiB155570 KiB-2.8%
Settled RSS137212 KiB131952 KiB-3.8%
Swap delta145347 KiB137652 KiB-5.3%

The startup and first-request differences clear both the absolute and relative materiality thresholds, and every paired comparison favors extracted. Memory and swap did not show a material winner. Extracted had slightly lower median RSS and swap, but the differences were below the experiment's materiality thresholds and were not consistently reproduced across measurements. Peak RSS favored extracted in five of six pairs; settled RSS in four of six. The roughly 7.7 MiB swap difference is far below the frozen 16 MiB plus 10% threshold.

What happens after startup?

Requests 2–6 were much closer than the first request. Their per-run median was 0.020696 s for fat and 0.019139 s for extracted (-7.5%), which did not meet the materiality rule. The large difference is therefore concentrated in the startup and first-request path rather than in steady warm requests.

Why might extraction help?

The extracted layout keeps the application classes and dependency JARs as ordinary filesystem entries instead of requiring the launcher and class loader to work through nested entries in one executable archive. This experiment shows a repeatable startup and first-request difference under memory pressure, but it does not isolate which individual class-loading or filesystem action causes the difference. That would require a different experiment.

Separate operational follow-up

After selecting extracted from the primary result, I ran it once more with StatLite. It was started only after six clean requests completed. The clean startup was 6.904 s; clean request 1 was 0.950558 s. StatLite then polled the application and its own loopback metrics every 30 seconds for one hour while a guest-local request ran at the same cadence.

All 120 scheduled requests succeeded with HTTP 200 and the expected 11,423 bytes. Latency ranged from 5.513 ms to 106.305 ms (mean 14.729 ms), and the latest request was only 100 ms behind schedule. The application kept one PID and shut down cleanly, with no fatal or OOM evidence. Guest-wide MemAvailable varied from 30,892 to 178,716 KiB; SwapFree varied from 354,236 to 520,100 KiB and ended at 518,916 KiB.

This follow-up is operational evidence, not a thirteenth comparison sample. The sampler's background shell did not receive the updated application and StatLite PID variables, so its per-process RSS columns are unavailable. That limitation is preserved in the journal and raw evidence; it was not worth repeating an hour-long run because it cannot change the primary comparison or the observed request and survival result.

What this means for production

The deployment pattern is straightforward:

build fat JAR
→ extract during packaging/deployment
→ run extracted application JAR + lib/

Extraction is especially easy to adopt when deployment already creates a container image, VM image, release directory, or other versioned application bundle. When startup time matters, the extracted layout is a sensible production choice to try first, particularly on constrained deployments. Memory and swap did not show a material winner, so extraction should not be sold as an RSS or swap optimization. Validate it against the actual application and runtime profile, and do not expect the same percentages for every Spring Boot application.

Limits and conclusion

This is one representative application on one constrained VM, with one JDK distribution and one fixed JVM profile. It does not establish a universal Spring Boot performance rule, nor does it measure deployment extraction cost as part of startup. The result is narrower and more useful: for this setup, Spring Boot's supported extracted layout produced a large, repeatable startup and first-request improvement, while later-request latency and memory/swap changes were not materially different.

Secondarily, the extracted layout made the already-demonstrated 256 MiB deployment smoother at the point where startup was most expensive. That is context for this test, not a separate claim about Spring Boot hosting.

The method, JVM flags, paired-run evidence, and operational follow-up are in the public experiment record. Full archives, databases, and private metadata are omitted.

Running Spring Boot on a small VPS? StatLite provides application metrics and basic host visibility without a separate Prometheus/Grafana stack.

Try StatLite ⭐ Star on GitHub Public experiment