Files
spring-boot-demo/spring-boot-startup-time/docs/05-jfr-instead-of-a-buffer.md
Ankur Mhatre 958b401f0f Spring Boot startup time: bean-by-bean diagnosis, and one directory per post
Adds spring-boot-startup-time/, the companion project for BLOG-618: a runnable
Spring Boot 4.1.1 application on JDK 25 that installs BufferingApplicationStartup
and FlightRecorderApplicationStartup behind a system property, and a /diag/startup
endpoint that computes step self time -- the number /actuator/startup does not give
you and the one that names the actual culprits.

Captured under docs/output/: the step tree sorted both ways, the same startup as JFR
events, a +5000-class experiment putting 0.11 ms per scanned class on the classpath
scan tax, the silent truncation a 2048-step buffer performs, and JDK 25 AOT cache
timings (6.93 s to 4.82 s). Post body and metadata live in post/.

Moves the existing Actuator project into actuator-in-production/ so the repository
holds one directory per article; the root README is now an index.
2026-09-05 00:17:37 +05:30

4.1 KiB

05 — JFR instead of a buffer

← prev 04 — Reading the step tree · next → 06 — The classpath-scan tax


FlightRecorderApplicationStartup lives in spring-core, needs no dependency, and emits each StartupStep as a JFR event. Swap the tracker and start a recording:

java -XX:StartFlightRecording=filename=startup.jfr,settings=profile,dumponexit=true \
     -Dstartup.tracking=jfr -jar app.jar

Everything below is from docs/output/03-jfr.txt.

The event type

@Name("org.springframework.core.metrics.jfr.FlightRecorderStartupEvent")
@Category("Spring Application")
@Label("Startup Step")
@Description("Spring Application Startup")
class FlightRecorderStartupEvent extends jdk.jfr.Event { ... }

In JDK Mission Control it appears under the Spring Application category. On the command line, the name matters more than you would expect:

  --events StartupEvent                     -> 0 events
  --events 'org.springframework.core.metrics.jfr.FlightRecorderStartupEvent' -> 398 events

jfr print --events matches the @Name, which is the fully qualified class name. The short form silently returns nothing — no error, no warning, just an empty result that reads like "Spring did not record anything".

Reading it without Mission Control

jfr summary startup.jfr
jfr print --events 'org.springframework.core.metrics.jfr.FlightRecorderStartupEvent' \
          --json startup.jfr

The eight slowest, sorted by the recording's own duration field:

      duration  name / tags
PT6.544849841S  spring.context.refresh
PT2.263845435S  spring.beans.instantiate beanName=&entityManagerFactory,...
PT1.500763631S  spring.context.beans.post-process
PT1.202591188S  spring.context.beandef-registry.post-process postProcessor=...ConfigurationClassPostProcessor@76b224cd
PT1.176668418S  spring.context.config-classes.parse classCount=130
PT0.538651275S  spring.beans.instantiate beanName=reportTemplateRegistry
PT0.535484510S  spring.beans.instantiate beanName=tariffCacheWarmer
PT0.515340537S  spring.boot.webserver.create factory=...TomcatServletWebServerFactory

(That run started in 7.441 s; the buffered run in chapter 04 started in 6.6 s. Same jar, different runs — see chapter 07 on why a single measurement is not a result.)

Note that this is the total time list from chapter 04, with spring.context.refresh on top and reportTemplateRegistry above tariffCacheWarmer. JFR gives you durations and parent ids; it does not compute self time either. The same subtraction applies.

What JFR buys you that the buffer does not

Correlation. The recording holds the JVM's own view of the same seconds:

 jdk.ExecutionSample            345
 jdk.GCPhasePauseLevel1         132
 jdk.GCPhasePause                39     (289.8 ms of pause in total)
 jdk.Compilation                 16
 jdk.ClassLoaderStatistics       10

If a bean's constructor is slow because a young collection landed in the middle of it, the buffer shows you a slow bean and JFR shows you why. That is the case for using it.

What it costs you

  • The recording is written to a file on the machine, not served over HTTP. In a container that means a volume or a copy out.
  • The tags are serialised into one flat String attribute, because JFR events only carry base types. Parsing beanName=x,beanType=y, is on you.
  • settings=profile records a great deal more than startup steps. For a 7-second startup that is fine; left on in production it is not.

Buffer or JFR?

Use the buffer when the question is "which bean", and you can reach the application over HTTP. Use JFR when the question is "why is that bean slow", when startup fails before the web server is up (the buffer is unreachable then; the JFR file is not), or when you need the same recording to answer a GC question.


← prev 04 — Reading the step tree · next → 06 — The classpath-scan tax