A CPU profile of a running service puts one JDK method at 72% — diagnose the hotspot
An order service pegs a CPU core at ~200 req/s. You attach a sampling profiler to the live JVM for 30 seconds in CPU mode and get the flat profile and hot stack below.
Constraints:
- the service only validates and stores orders; GC time is under 2% and the box is not swapping
- the JVM is fully warmed up — the profile was taken an hour after start
- assume the artifact is complete; you get no second capture
$ ./profiler.sh -e cpu -d 30 -f cpu.txt 8123
--- Execution profile ---
Total samples: 29841 (CPU 29720, non-CPU 121)
ns percent samples method
------ ------- ------- ------------------------------------------
21.4 s 71.7 % 21391 java.util.regex.Pattern.compile
3.1 s 10.4 % 3101 java.util.regex.Pattern$Curly.match
1.9 s 6.4 % 1902 java.lang.String.substring
--- Hot stack (21391 samples, 71.7 %)
[ 0] java.util.regex.Pattern.compile
[ 1] java.util.regex.Pattern.<init>
[ 2] java.util.regex.Pattern.compile
[ 3] com.acme.order.SkuValidator.isValid
[ 4] com.acme.order.OrderService.validate
[ 5] com.acme.order.OrderController.create
Diagnose the cause.
Pattern.compile dominates the profile and sits directly under SkuValidator.isValid, so a regex is compiled on every request — the code calls Pattern.compile, or String.matches/split, inside the request path. Compiling parses the pattern into a node tree and costs far more than matching with a ready one. Hoist it into a static final Pattern and only match per request.
- ✗Reading a hot JDK frame as a JDK problem instead of looking at the application frame directly beneath it
- ✗Confusing the cost of compiling a
Patternwith the cost of matching against one - ✗Assuming
String.matchesandString.splitare cheap — each compiles a freshPatternon every call
- →Why is
String.matches(regex)inside a loop just as costly as callingPattern.compilethere? - →What would a wall-clock profile of this service show that a CPU-mode profile hides?
Solution
Read the profile up the stack: a hot JDK method is never the culprit by itself — the application frame directly beneath it is. Here that frame is SkuValidator.isValid, and beneath it sits Pattern.compile. The pattern is being rebuilt on every request.
// ❌ before: a new Pattern per call
final class SkuValidator {
boolean isValid(String sku) {
return Pattern.compile("^[A-Z]{3}-\\d{4}-[A-Z0-9]{2}$")
.matcher(sku)
.matches();
}
}
// ✅ after: compiled once, only matching per request
final class SkuValidator {
private static final Pattern SKU =
Pattern.compile("^[A-Z]{3}-\\d{4}-[A-Z0-9]{2}$");
boolean isValid(String sku) {
return SKU.matcher(sku).matches();
}
}
Why it costs so much. Pattern.compile parses the expression and builds a node tree — orders of magnitude more work than running a Matcher over an already-built pattern. A Pattern is immutable and thread-safe, so a single instance in a static field is shared by every thread; only the Matcher is stateful, and that one is created per call.
The same trap hides behind String.matches, String.split and String.replaceAll — each calls Pattern.compile internally. In a hot path, replace them with a pre-compiled Pattern (or, for a plain delimiter, with indexOf).
Verify. Re-profile after the change: Pattern.compile should vanish from the top and the profile should flatten out.