JVM runtime analysis. Use when: profiling JVM performance, tuning GC, debugging memory leaks, finding dead code with production data. Not for general Java development or syntax questions.
Patterns for analyzing, optimizing, and debugging JVM applications ā both static and runtime.
Finding unused code?
āāā Have production traffic ā Runtime analysis (Scavenger)
āāā Need static-only analysis ā
ā āāā Simple ā ProGuard -printusage
ā āāā Custom analysis ā SootUp or ProGuard Core
āāā Just bug patterns ā SpotBugs, Error Prone
| Tool | Type | What It Does | When to Use |
|---|---|---|---|
| Scavenger | Runtime | Tracks actual usage in production | Have production data |
| ProGuard -printusage | Static | Lists unreachable from entry points | Know your entry points |
| SootUp | Library | Call graphs, data flow analysis | Building custom analysis |
| ProGuard Core | Library | Bytecode analysis primitives | Building custom tools |
| Dead Code Agent | Runtime | Tracks class loading | Quick prototype |
Key insight: No turnkey "run this, get dead code" CLI exists. You either:
SootUp provides call graph construction and analysis primitives. Useful for:
(Karakaya et al., TACAS 2024)
Gotcha: SootUp is a library, not a tool. You build analysis on top of it.
| Tool | Focus | Integration |
|---|---|---|
| SpotBugs | Bug patterns (400+) | Gradle/Maven, CI |
| Error Prone | Compile-time checks | javac plugin |
| NullAway | Null safety | Error Prone plugin |
Need production profiling?
āāā YES ā CPU or memory?
ā āāā CPU ā Need flame graphs?
ā ā āāā YES ā async-profiler
ā ā āāā NO ā JFR (built-in, zero config)
ā āāā Memory ā JFR (allocation profiling)
āāā NO (development only) ā VisualVM or IntelliJ Profiler
| Profiler | Overhead | Safepoint-Free | Output | Tradeoff | Best For |
|---|---|---|---|---|---|
| async-profiler | ~2% CPU | Yes | Flame graphs, JFR | Requires native agent attachment | Production CPU/allocation |
| JFR + JMC | ~1-2% | Partial (improved Java 16+) | Binary events | Less granular CPU data | Continuous monitoring |
| VisualVM | 5-10% | No | Various | Safepoint bias distorts results | Development only |
| IntelliJ Profiler | ~2% | Yes (uses async-profiler) | Flame graphs | IDE dependency | IDE-integrated |
(InfoQ 2025)
Why safepoint-free matters: JVM can only safely inspect threads at safepoints. JVMTI-based profilers (VisualVM, hprof) miss code between safepoints, skewing flame graphs toward safepoint-heavy code. async-profiler uses AsyncGetCallTrace to sample anytime (Wakart 2016).
Heap size?
āāā < 4 GB ā G1 (default since JDK 9)
ā WHY: ZGC/Shenandoah overhead not justified; G1's region-based collection efficient at this scale
āāā 4-32 GB ā Latency-sensitive?
ā āāā YES ā ZGC or Shenandoah
ā ā WHY: Concurrent marking/compaction keeps pauses <10ms regardless of heap size
ā āāā NO ā G1
ā WHY: G1's mixed collections handle this range well; simpler tuning
āāā > 32 GB ā ZGC (generational, JDK 21+)
WHY: ZGC's concurrent compaction scales linearly; G1 pauses grow with heap
| Collector | Pause Target | Heap Size | Tradeoff | JDK | Best For |
|---|---|---|---|---|---|
| G1GC | 200ms (tunable) | Any | Pauses scale with heap | 9+ default | General workloads |
| ZGC | <1ms | Large (100GB+) | ~15% throughput cost vs G1 | 15+ prod, 21+ gen | Latency-critical |
| Shenandoah | <10ms | Large | Higher CPU for barriers | 12+ (Red Hat) | Low-latency, older JDKs |
| Parallel | Max throughput | Medium | Stop-the-world only | All | Batch processing |
Why the thresholds:
Key insight: ZGC generational (JDK 21+) closes the throughput gap ā concurrent minor collections reduce allocation pressure (JEP 439).
(Oracle GC Tuning Guide, Shipilev JVM Anatomy Quarks, JEP 439)
OOM or suspected leak?
āāā Capture dump ā -XX:+HeapDumpOnOutOfMemoryError
āāā Analyze ā Eclipse MAT or HeapHero
ā āāā Run "Leak Suspects" report
ā āāā Check retained heap (not just shallow)
ā āāā Path to GC Roots (exclude weak refs)
āāā Fix ā Collections holding references, static fields, caches without eviction
Application hanging or slow?
āāā Capture ā jstack -l <pid> (or jcmd <pid> Thread.print)
āāā Take 3-5 dumps seconds apart
āāā Analyze:
ā āāā BLOCKED threads ā lock contention
ā āāā WAITING on same monitor ā bottleneck
ā āāā Same stack across dumps ā stuck thread
āāā Tools: FastThread.io, TDA, or manual grep
Why it happens: JVM can only safely inspect thread state at safepoints ā points where all threads are known to be in a consistent state. JVMTI's GetStackTrace requires this. Safepoints occur at method returns, loop back-edges, and allocation. Tight loops without allocations may run millions of cycles between safepoints, becoming invisible (Wakart 2015).
(Wakart 2015, async-profiler docs)
-XX:+DebugNonSafepoints for accurate frame resolution# At JVM start (recommended)
java -XX:+UnlockDiagnosticVMOptions -XX:+DebugNonSafepoints -agentpath:/path/to/libasyncProfiler.so ...
# Late attach works but misses already-compiled methods
-Xmx (native memory, metaspace, stacks, codecache)kubectl describe pod shows OOMKilled; native memory tracking shows usage# Good: percentage-based, container-aware
java -XX:MaxRAMPercentage=75.0 -XX:+UseContainerSupport ...
# Budget breakdown for 2GB container:
# Heap: ~1.5GB (75%)
# Metaspace: ~100MB (default MaxMetaspaceSize unbounded, set explicitly)
# Thread stacks: ~100MB (100 threads Ć 1MB default Linux stack)
# CodeCache: ~50MB (240MB reserved, typically uses ~50MB)
# Native/JNI: ~150MB buffer (JDBC drivers, compression libs, etc.)
Why 25-30%: Empirical guidance from production incidents. Exact overhead depends on workload ā NMT (Native Memory Tracking) gives precise breakdown for your app (Schatzl, Oracle GC team). Spring Boot apps with web frameworks often need closer to 30%; minimal services can use 20% (Datadog 2024).
(JEP 345, Datadog JVM Container Best Practices)
Why the pause: Full GC + heap traversal + I/O. The JVM must walk all live objects to write the dump. Parallel GC can speed traversal but I/O often dominates (Eclipse MAT FAQ).
-XX:+PrintCompilation shows unexpected inliningBlackhole.consume(), @State objects, sufficient warmup// Wrong: JIT may eliminate this
@Benchmark
public void bad() {
compute(); // No side effects, may be removed
}
// Correct: Blackhole prevents DCE
@Benchmark
public void good(Blackhole bh) {
bh.consume(compute());
}
| Scenario | Primary Tool | Alternative |
|---|---|---|
| Production CPU profile | async-profiler | JFR |
| Allocation hotspots | JFR | async-profiler --alloc |
| Memory leak | Heap dump + MAT | HeapHero |
| Deadlock | jstack -l | JMC thread analysis |
| GC issues | GC logs + GCViewer | JFR |
| Container sizing | NMT + metrics | VisualVM (dev) |
| Microbenchmarks | JMH | (no alternative) |
# async-profiler CPU flame graph
./profiler.sh -d 30 -f flamegraph.html <pid>
# JFR recording (no overhead until dump)
jcmd <pid> JFR.start duration=60s filename=recording.jfr
# Heap dump
jcmd <pid> GC.heap_dump /path/to/dump.hprof
# Thread dump
jcmd <pid> Thread.print > threads.txt
# Native memory tracking
java -XX:NativeMemoryTracking=summary ...
jcmd <pid> VM.native_memory summary
# G1 (default, balanced)
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
# ZGC (ultra-low latency, JDK 21+ generational default)
-XX:+UseZGC
# Shenandoah (low-latency, older JDKs)
-XX:+UseShenandoahGC
# Diagnostics
-Xlog:gc*:file=gc.log:time,tags
# Production container setup
-XX:+UseContainerSupport \
-XX:MaxRAMPercentage=75.0 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/dumps/ \
-XX:+ExitOnOutOfMemoryError
Load reference based on context:
| Detected | Load |
|---|---|
| Profiling, flame graphs, sampling | profiling.md |
| GC tuning, pause times, heap sizing | gc.md |
| Memory leaks, thread dumps, OOM | debugging.md |
| Kubernetes, containers, cgroups | containers.md |
| Obsolete | Replacement | Why |
|---|---|---|
-XX:+PrintGCDetails |
-Xlog:gc* |
Unified logging (JDK 9+) |
| VisualVM for production | async-profiler / JFR | Safepoint bias, overhead |
Manual -Xmx in containers |
MaxRAMPercentage |
Container-aware |
jmap -heap |
jcmd GC.heap_info |
jcmd preferred |
| hprof | JFR | hprof removed JDK 9+ |
| CMS | G1 or ZGC | CMS removed JDK 14 |
| Don't | Do | Why |
|---|---|---|
| Profile with default VisualVM in prod | Use async-profiler or JFR | Safepoint bias, overhead |
Set -Xmx = container limit |
Leave 25-30% for non-heap | OOMKilled by cgroup |
| Trust microbenchmarks naively | Use JMH properly | JIT optimizations mislead |
| Tune GC without measuring | Profile first, tune second | Premature optimization |
Use -XX:+PrintGCDetails (JDK 9+) |
Use unified logging -Xlog:gc* |
Old flags deprecated |
| Ignore safepoint bias | Check -XX:+DebugNonSafepoints |
Hidden hot spots |