A latency-sensitive JVM service shows recurring throughput dips, and its compilation output shows the same few methods being invalidated and recompiled over and over in steady state. How would you investigate whether repeated deoptimization is the cause, and what would you change?
answer
- confirm: JFR jdk.Deoptimization + PrintCompilation 'made not entrant'
- group by method + bci + reason
- steady-state recurrence ≠ bounded warmup burst
- look-alike: full code cache → flush + recompile, no deopts
- fix profile pollution / lazy loading / exception control flow before flags
basics
~20 sConfirm with evidence: JFR deoptimization events or -XX:+PrintCompilation "made not entrant" lines correlated with the dips, grouped by method, bytecode index and reason. Then treat the reason — megamorphic or unstable call sites, exceptions as control flow, late class loading — rather than tuning trap limits. Rule out code-cache exhaustion, which looks identical.
solid answer
~60 s**Confirm first.** Enable JFR and read `jdk.Deoptimization` events, or run with `-XX:+PrintCompilation` (and `-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation` for detail, viewable in an analysis tool). Aggregate by method, bytecode index and reason. Steady-state recompilation of the same method is the signature; a bounded warmup burst is not a problem. **Read the reason.** `class_check` points at a call site seeing new receiver types — polymorphism the profile can't pin down, often a shared abstraction, a proxy per type, or reflection. `unstable_if`/`unreached` points at a branch whose behaviour changed with traffic mix. `null_check` means nulls arriving where none were profiled. **Rule out look-alikes.** A full code cache causes flushing and endless recompilation with no deopts; check code-cache occupancy. Class loading in steady state (lazy plugins, generated classes per request) invalidates inlining repeatedly — that is a loading problem, not a profile problem. **Fix the cause.** Split a helper polluted by many callers so each site has a clean profile, reduce receiver variety at the hot site, stop throwing on common paths, pre-load and warm the real type set. Adjusting trap-limit flags is a last resort — HotSpot already withdraws speculation on its own.
code
text · 12 lines# lightweight, production-safe first pass
-XX:StartFlightRecording=settings=profile,filename=app.jfr
# read jdk.Deoptimization, jdk.Compilation, jdk.CodeCacheFull events
# more detail on a canary instance
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions -XX:+LogCompilation # hotspot.log, for an analysis tool
-Xlog:class+load=info # steady-state class loading?
-Xlog:safepoint,gc*=info # rule out GC / safepoint causes
# last-resort, measured, targeted only:
-XX:CompilerDirectivesFile=directives.jsongo deeper
Know that repeated recompilation can be observed with -XX:+PrintCompilation and that it usually reflects assumptions the JIT keeps having to abandon.
Describe collecting deoptimization events, grouping them by method and reason, and the usual causes: many receiver types at one site, branches becoming live, nulls appearing.
Run the full differential — GC, safepoints, code cache, class loading — then fix the cause: split polluted helpers, pre-load implementations, remove exception-based control flow, warm representative traffic.
Quantify the impact against the SLO before acting, prefer fixes that improve the design independently, and treat compiler flags as documented, benchmark-backed, upgrade-fragile last resorts.
## Step 1: establish that it is compilation at all Throughput dips have several common causes and they are distinguishable by evidence, not intuition. Before touching the JIT: - **GC?** Check the GC log for pauses of matching duration and frequency. Deopt dips do not correlate with allocation rate the way GC does. - **Safepoints?** `-Xlog:safepoint` reveals long time-to-safepoint or non-GC safepoint operations (revocation, deoptimization requests themselves) that stop all threads. - **Compilation?** `-XX:+PrintCompilation` timestamps, JFR's `jdk.Compilation` and `jdk.Deoptimization` events, and compiler-thread CPU usage. Overlay the timelines. Deoptimization-driven dips line up with clusters of `made not entrant` and re-compilations of the *same* methods. ## Step 2: characterize the deoptimizations The useful aggregation is (method, bytecode index, reason, count over time). - **Bounded and decaying** — a warmup phenomenon. Normal; either accept it or move it earlier with a warmup routine. - **Steady-state and recurring at the same site** — a real problem worth chasing. - **Correlated with class loading** — look at `-Xlog:class+load`. Something is loading classes in steady state: a lazily initialized plugin, per-type generated proxies or serializers, `Class.forName` on request data. Each new implementation can invalidate compiled code that assumed a unique implementor. Common reasons and what they mean: | Reason | Typical cause | |---|---| | `class_check` | receiver type at an inlined call site differs from the profile — polymorphism, proxies, per-tenant implementations | | `unstable_if` / `unreached` | a branch the profile called dead is now taken — changed traffic mix, feature flag flip, error path | | `null_check` | first nulls flowing where none were profiled | | `bimorphic` / `polymorphic` | a site outgrowing its guarded inline | | `constraint` / dependency invalidation | class loading broke a hierarchy assumption | ## Step 3: rule out the look-alikes - **Code cache exhaustion.** When the code cache fills, HotSpot flushes compiled methods and may disable compilation entirely (`CodeCache is full. Compiler has been disabled.`). The symptom — methods constantly recompiled, throughput sagging — mimics deopt churn, but there are no deopt events. Check code-cache usage via JFR/JMX and raise `ReservedCodeCacheSize` if it is genuinely tight. - **Compiler queue saturation.** Many classes loading at once starves compilation; methods run at lower tiers for long stretches. - **Profile pollution without deopt.** A megamorphic site simply compiles to a virtual dispatch — slower, but stable and deopt-free. That is a design cost, not churn. ## Step 4: change the cause, not the flags Ordered by how often they are the right answer: 1. **Reduce receiver variety at hot sites.** A generic helper called with dozens of implementations gets one shared, useless profile. Splitting it (duplicate the small helper, use per-type entry points, or make the site type-stable) restores monomorphic inlining. This is "profile pollution" and it is the most common real fix. 2. **Stop lazily loading in steady state.** Pre-create proxies, pre-load plugin implementations, eagerly initialize serializer/mapper caches so the hierarchy stabilizes during warmup. 3. **Stop using exceptions for expected outcomes.** Every rare-turned-common throw both costs directly and destabilizes a previously pruned path. 4. **Warm the real workload.** A warmup that exercises only the happy path leaves every alternate branch and type to be discovered in production. Warm with representative traffic, including errors and alternate types. 5. **Consider architectural stability.** If a hot path must be polymorphic across many types, accept virtual dispatch and optimize elsewhere — do not contort the design to chase inlining. 6. **Flags, last.** `-XX:PerMethodTrapLimit` / `-XX:PerBytecodeTrapLimit` change how quickly speculation is withdrawn; compiler directives (`-XX:CompilerDirectivesFile`) can disable inlining at a specific site. These are targeted, measurable-only-with-benchmarks interventions and they age badly across JDK upgrades. Never start here, and never ship one without a measurement and a comment explaining it. ## Step 5: decide whether it matters The honest principal-level answer includes a cost judgement. Deopt churn that costs a fraction of a percent of throughput is not worth a design change; the same churn producing multi-hundred-millisecond p99.9 outliers in a latency SLO is. Quantify the dip's contribution before restructuring code — and prefer changes that also improve the design (fewer megamorphic god-helpers, less lazy initialization, fewer exceptions on normal paths) over changes whose only justification is the compiler.
- What is profile pollution and why does it cause repeated deoptimization?A single shared method — a generic helper, a mapper, a logging wrapper — is called from many places with many receiver types, so its per-bytecode profile is an average of all callers and describes none of them. Inlining it into any one caller produces guards that keep failing, causing traps and recompiles. Splitting the helper so each hot site sees a stable type set restores useful profiles.
- How do you distinguish deoptimization churn from code-cache exhaustion, given both show constant recompilation?Code-cache exhaustion produces no deoptimization events; instead you see code-cache occupancy near the limit, flushing, and possibly the 'CodeCache is full. Compiler has been disabled' message. Deopt churn shows jdk.Deoptimization events clustered on specific methods and bytecode indices with a consistent reason.
- When is tuning trap-limit flags actually justified?Almost never as a first move — HotSpot already stops speculating at a site after repeated traps, so the flags mostly change how fast that happens. They are justified only when profiling has isolated a specific hot site, a code change is genuinely unavailable, and a benchmark on representative traffic shows a durable win; even then the setting should be documented and re-validated on every JDK upgrade.
- Why can lazily loading classes in steady state be worse than loading them at startup?Compiled code records dependencies on the loaded class hierarchy, such as an interface having a single implementor. Loading a new implementation invalidates that code, forcing not-entrant marking, on-stack deoptimization and recompilation — during traffic rather than during warmup. Loading everything up front confines the disruption to startup.
saying these in an interview costs you the question
- Jumping straight to JVM flags before confirming deoptimization is even involved
- Blaming GC for every latency dip without checking compilation and safepoint logs
- Treating any deoptimization as a defect, including the normal bounded warmup burst
- Missing code-cache exhaustion, which produces near-identical recompilation symptoms with no deopt events
- Restructuring application design around inlining without quantifying the dip's actual cost