Troubleshooting a Slow Java Application - Yenra

Collect a useful incident baseline, inspect threads and Flight Recorder evidence, and distinguish CPU work from blocking and database waits.

An inspection lens reveals an amber constriction inside a navy server model beside translucent diagnostic traces.
Conceptual illustration: find the constrained part of a request before choosing a performance fix.

Start with the slow operation and its time window. Then collect enough evidence to distinguish computation, allocation, blocked threads and downstream waits. This guide gives a practical incident sequence and a small Java Flight Recorder lab so you can practice interpreting evidence before using it on an important service.

Define the symptom and preserve a baseline

Record the affected endpoint or job, deployment version, start time and timezone, request rate, error rate and latency distribution. Compare against a similar healthy period. Include the JDK vendor/version, host or container limits, recent releases and downstream incidents. A report of “Java is slow” is difficult to test; “the order-summary endpoint's 95th-percentile latency rose during this deployment at the same request rate” provides a useful investigation boundary.

Separate wall-clock latency from CPU time. A request can spend most of its lifetime waiting on a connection pool, a lock or a remote service while consuming little CPU. Likewise, a high process CPU figure can represent healthy throughput or a saturated bottleneck. Relate each measurement to the same interval and workload.

Preserve a small reproducible request with synthetic or redacted data. Before changing settings, write down the hypothesis and the observation that would support or weaken it. This makes the later comparison more useful than tuning several JVM flags at once.

Collect a small, targeted evidence set

For a HotSpot JDK 25 process, use the matching JDK tools from the same machine and appropriate operating-system identity. Container PID namespaces and attach permissions can change what is visible. Start by listing local JVMs and asking the actual target which commands it supports:

jcmd -l
jcmd 12345 VM.version
jcmd 12345 help
jcmd 12345 Thread.print -l
jcmd 12345 Thread.dump_to_file -format=json threads.json
jcmd 12345 JFR.start name=incident settings=profile duration=60s filename=incident.jfr

Replace 12345 with the observed process ID. Choose a writable private output path and a unique capture filename. Thread.print gives a familiar platform-thread view; the newer Thread.dump_to_file format can include virtual threads. Record a few snapshots during the symptom to identify persistent patterns. Inspect jcmd 12345 help JFR.start if options differ on your selected JVM.

The jcmd manual documents command availability, permissions and impact. The profile recording configuration collects more detail than the default configuration and can add overhead. Start with a short scoped capture, watch service health and use your incident policy for production diagnostics.

Recordings and thread dumps can reveal paths, application names, arguments and other sensitive details. Store them with appropriate access. A heap dump can be much larger and expose object contents; request one only when the evidence points to a memory question and you have accounted for its operational cost.

Read evidence as competing explanations

On a narrow screen, scroll the table horizontally. Keyboard users can focus the table and use the arrow keys.

What to investigate next
ObservationPossible explanationCorroborating evidence
Busy CPU and repeated application stacksComputation dominatesExecution samples repeatedly point to the same hot methods during the slow interval.
Frequent allocation and disruptive GC activityAllocation pressure or retained dataAllocation sites, heap trend, pause events and the affected latency interval agree.
Threads repeatedly blocked on one monitorLock contentionSnapshots show the same owner and waiting code paths.
Low CPU while work waits for database connectionsPool exhaustion or long transactionsPool wait/active metrics and database session/transaction evidence.
Time accumulates in remote callsDependency or network delayPer-dependency timings, timeouts and correlated remote request logs.

Sample counts are evidence of where execution was observed, not exact invocation counts. A sleeping thread can be normal. A parked worker in an idle pool can be normal. Look for a pattern that explains the user-visible delay and is supported by more than one relevant signal. The JDK Flight Recorder lessons explain recordings and events.

Practice with waiting and CPU recordings

The downloadable ProfileDemo.java has two modes: repeated 100-millisecond sleeps or a repeated square-root calculation. Each runs for approximately 15 seconds. Use a development machine for the CPU mode. The program has been compiled and exercised with Java 25; it is a diagnostic teaching fixture, not a benchmark.

Download the profiling lab and README (ZIP)

javac ProfileDemo.java
java -XX:StartFlightRecording=filename=wait.jfr,settings=profile,dumponexit=true ProfileDemo wait
java -XX:StartFlightRecording=filename=cpu.jfr,settings=profile,dumponexit=true ProfileDemo cpu
jfr summary wait.jfr
jfr summary cpu.jfr
jfr print --events jdk.ThreadSleep wait.jfr
jfr print --events jdk.ExecutionSample cpu.jfr

The program prints its PID and chosen mode, followed by a completion message. The JDK also prints recording startup information. After each process exits, confirm its .jfr file exists and jfr summary can read it. In the waiting capture, look for ThreadSleep events associated with ProfileDemo.main. In the CPU capture, inspect execution samples for ProfileDemo.calculate. Exact event counts and stack inlining vary with machine and JDK.

The jfr command reference explains summary and print output. In a real capture, examine time ranges, stack traces and event thresholds rather than comparing raw totals from unrelated recording durations.

Change one cause and repeat the comparison

For a hot method, investigate its algorithm and inputs. For contention, inspect lock scope and ownership. For a database wait, inspect the query, transaction lifetime and database plan. For allocation pressure, identify the creating code and retained data before adjusting heap limits. The proposed change should follow from the evidence.

Repeat the same representative workload after the change. Compare latency, throughput, errors and resource use, and confirm the output is still correct. Keep the baseline, change and outcome together. If latency improved only because less work was accepted, record that tradeoff explicitly.

Escalate when evidence remains ambiguous: collect the missing downstream timing or reproduce a smaller case. Avoid presenting one clean recording as proof that every workload is fixed. Preserve the version and workload details needed for someone else to repeat the result.

Continue with a related task