Scenario Q&A: Symptom, Then Investigation
More than 55 performance questions in the form interviewers actually ask: a symptom with a number, and you walk the investigation. Every answer follows the same loop used across this series - measure, find the bottleneck, change one thing, prove it - and links back to the lab page where it was measured. Each card carries the model answer and the follow-up trap that separates memorisation from experience.
The Interviewer Wants a Procedure
A senior performance interview rarely asks "what does -Xmx do". It says: p99 doubled after the release, CPU is at 30%, what do you do? The interviewer is not looking for the cause - they cannot know it - but for a procedure that converges on the cause without guessing. Candidates fail by jumping to a favourite fix (a bigger pool, more instances, a GC flag) before they have a number.
Use the same six moves on every question. State the symptom with a number and a window. Pick the layer with USE (utilisation, saturation, errors) for resources and RED (rate, errors, duration) for services. Form one hypothesis that a command can falsify. Name the exact evidence (tool and command). Change one variable. Prove it with the same load and the percentile that hurt. The lab pages in this series practise exactly that loop on one service, OrderService (a Spring Boot 3 / Java 21 REST service on AWS).
| Prompt | Weak (a guess) | Strong (a procedure) |
|---|---|---|
| p99 is 3 s | Increase the heap and the thread pool. | Is it every request or the tail? Compare p50 vs p99, then a trace of a slow request to attribute the time; only then pick a layer. |
| Pods keep restarting | Give them more memory. | Exit code 137 / OOMKilled or a failed probe? If OOMKilled, was it heap or non-heap (NMT)? Only then size the container. |
| Throughput is flat | Add instances. | Which resource is saturated: app CPU, DB, pool, a lock? Adding instances to a shared-DB bottleneck changes nothing. |
Three Models That Answer Most Questions
Three small models answer a surprising fraction of questions. First, where is the time - attribution beats optimisation. Second, Little's Law (L = λW) ties latency, throughput and concurrency; see Measure First. Third, queues explode near saturation, so utilisation above about 70-80% buys latency spikes, not throughput.
| Signal | What it means |
|---|---|
| Latency up, CPU low | Something is waiting: a pool, a lock, a downstream, throttling. Reach for a thread dump or trace, not a CPU profile (Profiling Toolbox). |
| Latency up, CPU high | Something is computing or collecting: hot loop, serialization, GC. CPU profile and GC log. |
| Only p99 bad, p50 fine | A tail source: GC pause, lock convoy, cold cache, retries, fan-out amplification, throttling. |
| Throughput flat when load rises | A saturated shared resource (DB, pool) or a serial section (Amdahl / USL, Measure First). |
- Quoting average latency. Averages hide the tail; always give p95/p99 and the window.
- Optimising code that is 4% of the request. Do the attribution first.
- Treating "CPU is low" as "the system is fine". Low CPU plus high latency is the signature of waiting.
JVM, Memory & GC Scenarios
Ten questions on heap, GC, JIT, startup and profiling. For each, practise saying the first command out loud.
Put the spike times on a timeline and overlay candidates that have a 30 s period: GC (-Xlog:gc*), a scheduled job, a cache whose TTL is 30 s, a metrics scrape, JVM safepoints, CFS throttling. Whichever clock matches the spikes wins. If a Pause line sits under each spike, read its cause and size; if not, look at the application.
Follow-up trap: Assuming GC because it is the famous one. A synchronised cache expiry (stampede) or cron burst produces the same shape.
Deep dive: JVM Memory & GC
Container memory is heap plus metaspace, thread stacks, direct buffers, code cache, GC structures and native libraries. The kernel kills the process when the sum crosses the cgroup limit. Enable Native Memory Tracking (-XX:NativeMemoryTracking=summary, jcmd <pid> VM.native_memory summary), then set -XX:MaxRAMPercentage to leave headroom.
Follow-up trap: Raising -Xmx makes it worse: the heap now claims more of the same limit.
Deep dive: AWS Compute & the JVM
Look at the post-full-GC floor over hours: a rising floor is a leak, a stable floor with bigger peaks is load. Take jcmd <pid> GC.class_histogram twice a few minutes apart and diff, then a heap dump and the dominator tree for the retained set. Classic culprits: static maps, listeners never removed, ThreadLocals in pooled threads.
Follow-up trap: Forcing GC to "see" usage. A heap that fills between collections is normal.
Deep dive: JVM Memory & GC
The JVM is container aware (cgroup v2 since JDK 15 in addition to v1). The default MaxRAMPercentage is 25, so about 256 MiB, and below roughly 1792 MiB and 2 CPUs the JVM picks SerialGC (not "server class"). This is measured on the AWS Compute & the JVM page.
Follow-up trap: Setting -Xmx to the container limit. Leave 25-40% for non-heap.
Deep dive: AWS Compute & the JVM
Cold JVM: interpreted code, C1 then C2 compilation, class loading, empty connection pools and caches. Fixes: readiness gate with a warm-up, AppCDS, lazy init off for hot paths, slow-start in the load balancer, SnapStart or CRaC where available. Measure the warm-up curve instead of asserting it.
Follow-up trap: Claiming a native image is automatically faster: startup and memory improve, peak throughput usually does not.
Deep dive: JIT, Startup & Native
top -H -p <pid> to find the hot thread, convert its id to hex and match nid=0x... in jcmd <pid> Thread.print; or record 30 s of JFR and read the top frames. If the hot threads are GC workers, it is allocation, not business logic.
Follow-up trap: Restarting first. You destroy the evidence.
Deep dive: Profiling Toolbox
Default G1 for balanced services. Parallel when only throughput matters (batch). ZGC (generational in JDK 21 with -XX:+ZGenerational, default generational in JDK 23) for very low pause targets on larger heaps. Decide from a measured pause and throughput comparison on your workload.
Follow-up trap: Quoting pause numbers from a blog; collectors trade CPU for pause, so the answer depends on spare cores.
Deep dive: JVM Memory & GC
Class loading without unloading: a classloader leak (redeploys, dynamic proxy or bytecode generation per request). Class histogram and jcmd <pid> VM.metaspace; cap with -XX:MaxMetaspaceSize only after finding the leak.
Follow-up trap: Raising the cap, which only delays the crash.
Deep dive: JVM Memory & GC
A CPU profile only samples running threads. A service that is slow but idle is waiting; use a wall-clock profile or thread dumps and look for BLOCKED/WAITING states and the lock or call they wait on.
Follow-up trap: Trusting a single profile type. Match the profile (CPU, allocation, lock, wall) to the question.
Deep dive: Profiling Toolbox
Find the allocation sites with a JFR allocation profile (ObjectAllocationSample), then remove waste: boxing in hot loops, repeated string building, per-request big buffers, JSON trees. GC cost scales with survivors, but allocation rate sets pause frequency.
Follow-up trap: Tuning GC flags before reducing allocations.
Deep dive: JVM Memory & GC
- Giving a flag recipe ("set -Xmx and -XX:+UseG1GC") without evidence. State what log or profile would justify it.
- Forgetting that JVM version changes the answer: generational ZGC (JDK 21 opt-in), virtual-thread pinning fixed in JDK 24.
Threads, Pools & Virtual Threads
Concurrency answers combine a formula and a symptom. The virtual-thread claims below are version specific; always state the JDK.
Apply Little's Law: concurrency = rate x time in system. If time in system grew (a slow downstream), the same arrival rate needs more threads, so the pool saturates. Take two thread dumps 10 s apart and see what the threads wait on; fix the slow dependency or add a timeout, do not just enlarge the pool.
Follow-up trap: Raising pool size moves the queue downstream and can overload the database.
Deep dive: Concurrency & Virtual Threads
CPU-bound: about the number of cores (N+1). IO-bound: N x (1 + W/C), where W is wait and C is compute time per task; example from the Capacity run: 4 cores, 90 ms wait, 10 ms compute gives 40 threads. Then cap by the downstream you protect (DB connections) and validate under load.
Follow-up trap: Using the formula without measuring W and C.
Deep dive: Concurrency & Virtual Threads
No. They make blocking IO cheap (huge concurrent waiters on few carrier threads), so they help fan-out and thread-per-request services. They do nothing for CPU-bound work, and a database pool is still bounded, so protect it with a Semaphore. On JDK 21 synchronized around blocking pins the carrier (fixed in JDK 24, JEP 491). Boot 3.2+: spring.threads.virtual.enabled=true.
Follow-up trap: Claiming they remove the need for timeouts and bulkheads.
Deep dive: Concurrency & Virtual Threads
Lock contention. The thread dump names the monitor (waiting to lock <0x...>) and the owner. JFR's jdk.JavaMonitorEnter events confirm. Fix by shrinking the critical section, lock striping, or concurrent data structures, not by adding threads.
Follow-up trap: Confusing BLOCKED (monitor) with WAITING/TIMED_WAITING (parked, sleeping).
Deep dive: Profiling Toolbox
It runs on the shared common pool whose size derives from the CPU count. Concurrent requests contend for the same few workers, one slow task stalls others, and in a CPU-limited container the pool can be tiny. Use a dedicated executor with a bound.
Follow-up trap: Assuming parallel always means faster; for small collections overhead dominates.
Deep dive: Concurrency & Virtual Threads
Without an executor argument, the common ForkJoinPool (sized from availableProcessors()). Blocking IO inside it starves unrelated work. Pass an explicit executor for blocking tasks.
Follow-up trap: Using the default pool for blocking calls.
Deep dive: Concurrency & Virtual Threads
Past the saturation knee, extra threads add context switching, cache misses and queueing at the shared resource. Throughput stays flat while latency rises (the Measure First USL curve).
Follow-up trap: Not mentioning a measurement that proves it: run the same load at 2 pool sizes.
Deep dive: Measure First
Threads repeatedly queue for one hot lock and get descheduled while holding or waiting, so the lock is idle between hand-offs and throughput collapses while CPU is low. Visible as many BLOCKED threads on one monitor.
Follow-up trap: Saying "deadlock"; a convoy makes progress, just slowly.
Deep dive: Concurrency & Virtual Threads
- Using preview APIs as if stable: structured concurrency and scoped values are still preview in JDK 21 (
--enable-preview). - Presenting thread-count formulas as constants. They are starting points that load tests must confirm.
Spring, HTTP & Network Scenarios
Spring and the network path between the user and your handler. Many of these are 20-minute fixes once you know what to look at.
Classic keep-alive mismatch: the ALB idle timeout (60 s by default) is longer than the app's keep-alive timeout, so the app closes an idle connection and the ALB reuses the dead socket. Make the app timeout longer than the ALB one (server.tomcat.keep-alive-timeout in Spring Boot). Check the load balancer access logs for the 502 with no target response.
Follow-up trap: Confusing 502 (bad or closed connection) with 504 (target did not answer in time).
Deep dive: Spring, HTTP & Network
Threads are blocked on IO: database, downstream HTTP, a lock. Thread dump shows where. Fix with timeouts, caching, async or virtual threads; raising server.tomcat.threads.max only adds waiters.
Follow-up trap: Treating busy threads as CPU work.
Deep dive: Spring, HTTP & Network
DNS caching. The JVM caches positive lookups (default 30 s without a security manager; networkaddress.cache.ttl), and connection pools keep sockets to the old node. Lower the TTL, set a max connection lifetime, and validate connections.
Follow-up trap: Blaming the cloud; check the JVM and pool behaviour first.
Deep dive: Spring, HTTP & Network
Not by itself. It uses fewer threads for IO-bound, high-concurrency workloads, but with a blocking database driver you gain little, and CPU-bound code is no faster. Since Java 21, MVC with virtual threads often delivers the concurrency benefit with simpler code. Decide with a load test of your own workload.
Follow-up trap: Rewriting to reactive without measuring.
Deep dive: Spring, HTTP & Network
Connection reuse. A new TCP (and TLS) handshake per call costs round trips; a pooled keep-alive client avoids them. Check that the HTTP client is a shared singleton with a pool and that all three timeouts (connect, read, overall response) are set.
Follow-up trap: Creating a new client instance per request.
Deep dive: Spring, HTTP & Network
Return smaller DTOs (do not serialize entities with lazy graphs), avoid pretty-printing, reuse the ObjectMapper, compress large payloads, paginate. Prove it with an allocation + CPU profile before and after.
Follow-up trap: Switching libraries before shrinking the payload.
Deep dive: Spring, HTTP & Network
Measure what the 40 s is: classpath scanning, auto-configuration, bean creation, connection warm-up. Remedies: lazy init for rarely used beans, AppCDS, Spring AOT, a startup probe so traffic arrives only when ready, and scaling on the right signal early enough. See Scaling & Observability.
Follow-up trap: Optimising startup while the real delay is the JIT warm-up after readiness.
Deep dive: JIT, Startup & Native
Liveness answers "restart me?" and should not check dependencies (a database blip would restart every pod). Readiness answers "send me traffic?" and may reflect warm-up and dependency health. Use a startup probe for slow boots.
Follow-up trap: Pointing liveness at a deep health check, causing restart storms.
Deep dive: Scaling & Observability
- Tuning Tomcat before looking at the downstream calls the threads wait on.
- Forgetting that the load balancer has its own timeouts; 502/504 are often about the boundary, not the handler.
Database Scenarios
Database questions are the most common because the database is usually the 650 ms in the 800 ms request.
Attribute before tuning: a distributed trace or span breakdown (or Performance Insights for the database) splits the request into app, database, downstream and serialization. In the example 650 ms are the database; only then examine its queries, count per request, and plans.
Follow-up trap: Starting with a CPU profile; the time is spent waiting, not computing.
Deep dive: Database Performance
Requests wait longer than connectionTimeout for a connection. Check hikaricp.connections.pending and acquire time, look for long transactions or leaked connections (leakDetectionThreshold), slow queries holding connections, and whether pool x instances exceeds the database limit.
Follow-up trap: Just raising maximumPoolSize, which can saturate the database.
Deep dive: Database Performance
Count statements per request (our capstone counter showed 81 per request). Fix with a join fetch, @EntityGraph, batch fetching (hibernate.default_batch_fetch_size), or a DTO projection. Verify the statement count dropped and the latency followed.
Follow-up trap: Using EAGER fetching, which hides N+1 rather than removing it.
Deep dive: Database Performance
Possible causes: a function or cast on the column, a type mismatch, a low-selectivity predicate, wrong composite column order, stale statistics, or the planner chose a scan because it is cheaper. Read the plan (estimated vs actual rows) instead of guessing.
Follow-up trap: Adding indexes blindly: each one slows writes and consumes memory.
Deep dive: Database Performance
The database still reads and discards all skipped rows. Use keyset (seek) pagination: WHERE (created_at, id) < (?, ?) ORDER BY created_at DESC, id DESC LIMIT 20 with a matching index.
Follow-up trap: Forgetting a stable tiebreaker column.
Deep dive: Database Performance
A database has a finite number of useful concurrent executions (cores, IO). Beyond that, extra connections queue inside the database and add contention. Size the pool from the database side and use the pending metric to tell starvation from saturation.
Follow-up trap: Quoting the formula without measuring.
Deep dive: Database Performance
They offload reads and add capacity, but replication lag gives stale reads: read-your-writes needs routing the next read to the primary or tolerating staleness. They do not help write-heavy bottlenecks.
Follow-up trap: Sending every read to the replica including post-write reads.
Deep dive: Database Performance
Transactions held open across slow work (remote calls inside a @Transactional method) hold connections and locks. Narrow transaction scope; do remote calls outside it; set statement and idle-in-transaction timeouts.
Follow-up trap: Keeping HTTP calls inside the transaction boundary.
Deep dive: Database Performance
- Giving numbers that were not measured. The plan outputs in the Database Performance page are labelled illustrative where they are not from this sandbox.
- Treating the ORM as the problem when a missing index or a long transaction is.
Caching, Messaging & Resilience Scenarios
Caching and distributed-systems questions are mostly about what multiplies: misses, retries, fan-out, queues.
Cache stampede: many requests miss simultaneously and all recompute. Fixes: single-flight (one loader per key), jittered TTLs, stale-while-revalidate, or refresh-ahead. Measure hit ratio and backend load around expiry.
Follow-up trap: Making TTLs identical, so every key expires together.
Deep dive: Caching
Effective latency = h x t_cache + (1 - h) x t_db. With a 20 ms backend and a 1 ms cache, 90% hit gives about 2.9 ms; 50% gives 10.5 ms. Compute it for your numbers and watch the miss path, which sets the tail.
Follow-up trap: Ignoring the cost and staleness of invalidation.
Deep dive: Caching
Tail amplification: with five calls that each have a 1% slow tail, about 4.9% of requests hit at least one (Capacity run). Reduce fan-out, use hedged requests for idempotent reads, set timeouts from a latency budget, and cache the slow ones.
Follow-up trap: Adding retries without a budget, which adds load to the slow dependency.
Deep dive: Distributed Systems & Messaging
When a dependency slows, clients time out and retry, multiplying load so it cannot recover. Use exponential backoff with jitter, a retry budget, per-call timeouts, a circuit breaker and load shedding. Retry only idempotent operations.
Follow-up trap: Retrying at every layer: multiplying factors (3 x 3 x 3).
Deep dive: Distributed Systems & Messaging
The visibility timeout expired before processing finished, so the message became visible again. Set the timeout above worst-case processing time, extend it, and make handlers idempotent (SQS standard is at-least-once). Use a DLQ for poison messages.
Follow-up trap: Assuming exactly-once delivery.
Deep dive: Distributed Systems & Messaging
Compare produce rate to consume rate. If consumers are slow, profile the handler (usually a downstream call); then scale consumers up to the partition count, raise max.poll.records carefully, or batch. Watch rebalances caused by slow polls (max.poll.interval.ms).
Follow-up trap: Adding consumers beyond the number of partitions; the extras sit idle.
Deep dive: Distributed Systems & Messaging
From a latency budget: the outer timeout must exceed the sum of inner timeouts and retries, and a deadline propagates downward so nobody works on a request the caller already abandoned.
Follow-up trap: Equal default timeouts at every hop.
Deep dive: Distributed Systems & Messaging
When demand exceeds capacity faster than you can add it or a shared dependency is the bottleneck. Reject early (429/503) the work you cannot finish, protecting latency for the work you accept, and prefer failing fast to queueing unbounded.
Follow-up trap: Unbounded queues: they convert overload into huge latency.
Deep dive: Capacity & Failure
- Describing exactly-once delivery as a property of the broker; in practice you get at-least-once and design idempotency.
- Using a cache to hide a slow query instead of fixing the query when the hit ratio will be low.
AWS Compute & Scaling Scenarios
AWS questions blend infrastructure knobs with JVM behaviour. The container experiments (memory limit, CFS throttling, available processors) are on AWS Compute & the JVM.
Suspect CFS throttling. A CPU limit enforces a quota per 100 ms period; bursts across many threads (GC, JIT, request bursts) exhaust it and freeze all threads. Check container_cpu_cfs_throttled_periods_total or cgroup cpu.stat (nr_throttled). Our container demo measured it. Options: raise or remove the CPU limit, keep requests, reduce threads.
Follow-up trap: Looking only at average CPU utilisation.
Deep dive: AWS Compute & the JVM
Find the shared saturated resource: database CPU/IOPS/connections, a lock, a downstream rate limit, or a sticky load balancer. Check the connection budget: instances x pool must fit max_connections (9 instances x 20 = 180 in the example). Scaling stateless tiers cannot lift a shared bottleneck.
Follow-up trap: Scaling harder, which often makes the shared resource worse.
Deep dive: Capacity & Failure
Scale-out has lag: metric period, cooldown, task start, image pull, JVM warm-up. Scale on a leading signal (request rate per target, queue depth) rather than CPU alone, keep headroom, use step or predictive policies, and make startup faster.
Follow-up trap: Tuning the threshold when the real lag is the 90 s startup.
Deep dive: Scaling & Observability
It is the cold start: init plus class loading plus framework wiring. Use SnapStart for supported Java runtimes, provisioned concurrency to keep environments warm, trim dependencies, avoid heavy frameworks, and raise memory (CPU scales with memory). Measure cold vs warm separately.
Follow-up trap: Believing more memory is always more cost; shorter duration can offset it.
Deep dive: AWS Compute & the JVM
Often a price/performance win (arm64), but you must have arm64 images (multi-arch build), arm64 native libraries, and you should load-test your workload on both. Compare cost per request, not instance price.
Follow-up trap: Skipping load tests because the JVM is portable.
Deep dive: Cost & Capstone
The health check endpoint is probably doing too much (deep dependency checks) or is queued behind slow requests, so a loaded but working task fails health checks and is killed. Keep health checks shallow and fast, tune thresholds, and separate liveness from readiness.
Follow-up trap: Health check path that hits the database.
Deep dive: Scaling & Observability
502: the target returned a malformed response or closed the connection. 503: no healthy targets (or the service shed load). 504: the target did not respond within the idle timeout. Each points to a different layer.
Follow-up trap: Treating all of them as "the app is down".
Deep dive: Spring, HTTP & Network
SIGKILL, usually the OOM killer because the task memory limit was exceeded (check the stopped reason "OutOfMemoryError: Container killed due to memory usage"). Compare heap settings with the task memory; account for non-heap memory.
Follow-up trap: Reading 137 as an application crash and searching app logs only.
Deep dive: AWS Compute & the JVM
It adds latency (a millisecond or so, deployment specific) and a per-GB charge, and chatty services multiply it. Prefer AZ-local routing where practical, and cache or batch calls across AZ boundaries.
Follow-up trap: Ignoring the data transfer line item in the bill.
Deep dive: Cost & Capstone
- Quoting AWS limits or prices from memory; they change. Say "check the current documentation" and show how you would compute it.
- Treating Lambda, Fargate and EC2 as interchangeable; the performance levers (cold start, quota, warm-up) differ.
Capacity, Method & Cost Scenarios
The last group is estimation and judgement. Show the arithmetic, then say what you would validate.
Compute utilisation at 2x: if you run at 50% today you reach 100% and latency explodes (S/(1-ρ)). Then check the shared limits: DB connections, DB CPU, downstream quotas, and scale-out lag. Answer with numbers, then a load test to validate.
Follow-up trap: Assuming linear scaling.
Deep dive: Capacity & Failure
Little: 5,000 x 0.05 = 250 requests in flight. If an instance handles 40 concurrently at target utilisation, you need about 7, plus headroom for N+1 failure/deploy surge. Then check the database budget.
Follow-up trap: Forgetting headroom and the failure of one AZ.
Deep dive: Capacity & Failure
Typical gaps: data volume (indexes behave differently), no think time or unrealistic key distribution, a closed workload model that hides queueing (coordinated omission), a cold cache, a smaller dependency. Make the test resemble production.
Follow-up trap: Trusting a test that waits for each response before sending the next.
Deep dive: Benchmarking & Load Testing
Averages hide the tail. Show p95/p99 per endpoint and per customer segment, histogram buckets, and error rate. Define SLIs and SLOs and alert on burn rate.
Follow-up trap: Alerting on average latency.
Deep dive: Scaling & Observability
Same load, same data, same environment, repeated runs, report the percentile that hurt, and change one variable at a time. In the capstone each step shows a before/after row so the gain is attributable.
Follow-up trap: Changing three things in one deploy and claiming credit.
Deep dive: Cost & Capstone
When the code is not on the critical path, the gain is below the noise, or the SLO is already met and the change adds risk. Measure the headroom first.
Follow-up trap: Premature micro-optimisation (and benchmarking without JMH discipline).
Deep dive: Benchmarking & Load Testing
Database 5x slower, cache down, consumer lag, a downstream timing out, 10x spike, a leaking deploy. For each: expected symptoms, blast radius, protections (timeouts, bulkheads, shedding) and a game day or fault-injection test.
Follow-up trap: Documenting scenarios but never running them.
Deep dive: Capacity & Failure
Cost per request = hourly cost / sustained requests per hour. Raise throughput on the same infra (fix N+1, indexes, caching), right-size, use Graviton and commitments, and cut idle headroom without breaking the SLO. See the capstone.
Follow-up trap: Cutting instance size without checking tail latency.
Deep dive: Cost & Capstone
- Giving a precise-looking number without stating assumptions (service time, target utilisation, failure headroom).
Run the Interview Calculator
Most numeric answers in this chapter come from five formulas. This plain-Java program (no dependencies) computes them; compile-free run with java Capacity.java on Java 21.
Change one input at a time and say aloud what changes: raise the wait time to 180 ms and the pool formula says 76 threads, but the database connection budget still caps you at 9 instances of 20. That conversation - formula, then limit - is the interview.
Concept Check Table
A one-line answer for each classic concept question. Say it, then add one number or one tool.
| Question | One-line answer |
|---|---|
| What does p99 mean? | 99% of requests were at least this fast; 1 in 100 is slower. |
| Little's Law? | L = λ x W: concurrency = arrival rate x time in system. |
| USE vs RED? | USE (utilisation, saturation, errors) for resources; RED (rate, errors, duration) for requests. |
| Coordinated omission? | A closed-loop tester stops sending while the system is stalled, hiding the stall. Use an open model and time from the intended start. |
| G1 vs ZGC? | G1: balanced, region-based. ZGC: concurrent, very low pauses; generational in JDK 21 (opt-in), default in JDK 23. |
| Why metaspace? | Class metadata lives off-heap; grows with loaded classes. |
| What is TLAB? | Thread-local allocation buffer: lock-free bump-pointer allocation inside eden. |
| Virtual thread carrier? | A platform thread on which virtual threads are mounted; blocking IO unmounts them. |
| What pins a virtual thread (JDK 21)? | Blocking inside synchronized or a native frame; fixed for synchronized in JDK 24. |
| Hikari pending metric? | Threads waiting for a connection; non-zero means starvation. |
| Cache-aside? | App reads cache, on a miss loads the DB and fills the cache. |
| Idempotent? | Repeating the call has the same effect: required before retries. |
| Why jitter? | Randomised delay de-synchronises clients so retries do not arrive together. |
| What is SnapStart? | Lambda restores a snapshot of an initialised function to cut cold start (supported Java runtimes). |
| MaxRAMPercentage default? | 25% of container memory (heap max). |
| CFS quota? | Container CPU limit enforced as milliseconds of CPU per 100 ms period. |
- Reciting a definition and stopping. Add how you would observe it (metric, log, command).
Cheat Sheet, Recap & Where Next
| Symptom | Likely cause | First tool |
|---|---|---|
| Periodic p99 spike | GC, cron, synchronised TTL, throttling | gc log, cpu.stat, timeline overlay |
| OOMKilled, heap half full | Non-heap memory | NMT, container metrics |
| Low CPU, high latency | Waiting: pool, lock, downstream | thread dump, trace |
| Connection is not available | Pool starvation, long txns | Hikari metrics, leak detection |
| Flat throughput with more instances | Shared bottleneck | DB metrics, USL, connection budget |
| Sporadic 502 | Keep-alive mismatch | LB logs, keep-alive timeouts |
| Slow only after deploy | Cold JVM / caches | warm-up curve, readiness |
- Procedure beats recall: symptom with a number, layer, one hypothesis, evidence, one change, proof.
- Attribute the time before optimising: 650 of 800 ms was the database.
- Low CPU plus high latency means waiting; high CPU means compute or GC.
- Tails multiply: fan-out, retries, queues and utilisation near 100%.
- Know the version caveats: JDK 21 vs 24 pinning, generational ZGC, preview APIs.
- Measure before and after, one variable at a time, and say what you could not measure.
- Previous: Cost & Capstone
- Next: Measure First
- Related: Measure First · AWS Compute & the JVM · Cost & Capstone
- Hub: Performance hub (symptom, cause, tool, fix)
- Framework-level questions (IoC, proxies, transactions, security): Spring Interview Q&A