技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
jvm-diagnostics【数据与研究】Diagnoses JVM production incidents including OutOfMemoryError root causes, thread deadlock detection, heap leak analysis via MAT, and crash log triage for Java 17+ applications.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: jvm-diagnostics description: Diagnoses JVM production incidents including OutOfMemoryError root causes, thread deadlock detection, heap leak analysis via MAT, and crash log triage for Java 17+ applications. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: jvm diagnostics, thread dump, heap dump, oom error, deadlock detection, jstack analysis, MAT leak suspect, GC thrashing archetypes: - diagnostic - tactical anti_triggers: - brainstorming - vague ideation - code golf response_profile: verbosity: low directive_strength: high abstraction_level: operational role: review scope: review output-format: analysis content-types: - guidance - examples - do-dont related-skills: jvm-performance-tuning, framework-performance-tuning, async-programming
Diagnose and triage JVM production incidents using thread dumps, heap dumps, GC logs, and crash analysis. This skill covers reactive troubleshooting — when a production system is failing, degraded, or crashed. For proactive performance tuning and GC configuration, use jvm-performance-tuning instead. All diagnosis follows measurement-first principles: collect evidence, classify the root cause category, then recommend targeted fixes.
jcmd <pid> Thread.print) before any restart — it is your only live state recordjcmd <pid> GC.heap_dump /tmp/dump.hprof) for OutOfMemoryError incidents before restartinghs_err_pid*.log crash files for native memory issues, stack overflow, or JVM internal errorsUse this skill when:
OutOfMemoryError and requires heap dump analysis to identify the memory leak sourcehs_err_pid*.log file requiring crash analysisAvoid this skill for:
jvm-performance-tuning for GC selection, memory configuration, and allocation optimizationjcmd, JFR, MAT) are for live incident analysisCollect Diagnostic Evidence — Before making any changes or restarting the application, capture all available diagnostic data:
# Find the Java process PID
jps -l
# Thread dump (all threads with full stack traces)
jcmd <pid> Thread.print > threads_$(date +%s).txt
# Heap dump (binary HPROF format for MAT analysis — critical for OOM)
jcmd <pid> GC.heap_dump /tmp/heapdump_$(date +%s).hprof
# Current JVM flags (to confirm active GC and memory settings)
jcmd <pid> VM.flags > vmflags_$(date +%s).txt
# Memory and GC statistics snapshot
jstat -gcutil <pid> 1000 5 > gc_stats_$(date +%s).csv
# Check for crash logs (native crashes produce hs_err_pid*.log in the working directory)
ls -la /path/to/app/working/dir/hs_err_pid*.log
Checkpoint: Verify all diagnostic files were created and have non-zero size. If jcmd fails, check process permissions — it requires running as the same user as the Java process or as root.
Classify Incident Category — Analyze collected data to determine the primary incident category. Only one category should be the primary focus; secondary categories often stem from the primary:
BLOCKED threads waiting on monitors, or "Found one Java-level deadlock" in jcmd outpuths_err_pid*.log exists — analyze the Native frames and # Problematic frame lines for root causeCheckpoint: The incident category determines which deep-dive analysis to perform. Misclassifying a memory leak as GC thrashing (by increasing heap instead of fixing the leak) delays the real fix and wastes recovery time.
Deep-Dive Analysis — Perform targeted analysis based on the classified incident:
For OutOfMemoryError:
# Load HPROF file into Eclipse Memory Analyzer (MAT):
mat /tmp/heapdump_*.hprof
# Generate Leak Suspects Report (automated analysis):
# In MAT GUI: Report → Leak Suspects Report
# Key MAT metrics to examine:
# - Dominator Tree: which objects hold the most retained heap?
# - Thread Overview: which threads retain which objects?
# - Top Consumers: which classes allocate the most memory?
For Deadlocks:
# jcmd automatically detects Java-level deadlocks in thread dump output:
jcmd <pid> Thread.print | grep -A 15 "deadlock\|BLOCKED"
# Look for circular lock dependency chains:
# Thread A holds Lock X, waits for Lock Y
# Thread B holds Lock Y, waits for Lock X → DEADLOCK
For GC Thrashing:
# Analyze GC log for patterns:
grep -E "GC( pause|Heap resize)" /var/log/app/gc.log | tail -50
# Check if collections reclaim any heap:
# If collection time > 90% of wall-clock time and heap usage doesn't decrease → leak or capacity issue
# Use jstat to see GC history in real-time:
jstat -gcutil <pid> 1000 20
Identify Root Cause — Based on deep-dive analysis, determine the specific root cause within the incident category. Memory issues fall into these sub-types (ranked by frequency in production incidents):
Map or Set without eviction limits (most common OOM cause)List, Map, or Set accumulates objects forever with no bounded size or periodic flushString.intern() retain every unique string in the StringTable (permanent heap retention)Thread issues fall into these sub-types:
Recommend Targeted Fixes — Provide specific remediation steps based on the root cause classification:
// ── Root Cause: Unbounded Cache → Fix: Bounded Cache with Eviction ─────────────
// ❌ BAD: HashMap grows without limit — OOM when memory exhausted
private static final Map<String, Object> cache = new HashMap<>();
public void put(String key, Object value) {
cache.put(key, value); // Never removed
}
// ✅ GOOD: Bounded LinkedHashMap with LRU eviction
private static final Map<String, Object> boundedCache = new LinkedHashMap<>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 10_000; // Evict oldest when cache exceeds 10k entries
}
};
// ── Root Cause: ThreadLocal Leak → Fix: Always Call remove() in finally Block ───
// ❌ BAD: ThreadLocal in thread pool holds references indefinitely
private static final ThreadLocal<UserContext> context = ThreadLocal.withInitial(UserContext::new);
public void handleRequest(Request req) {
UserContext ctx = context.get();
ctx.parse(req);
process(ctx);
// FORGOT: context.remove() — next pool thread reuses stale data AND memory
}
// ✅ GOOD: Always clear ThreadLocal in finally block
public void handleRequestSafe(Request req) {
UserContext ctx = safeContext.get();
try {
ctx.parse(req);
process(ctx);
} finally {
safeContext.remove(); // Prevents leak to next task in pool
}
}
// ── Root Cause: Static Collection Growth → Fix: Bounded Buffer with Periodic Flush ──
// ❌ BAD: Static list accumulates all processed items forever
private static final List<ProcessedRecord> records = new ArrayList<>();
public void process(String data) {
records.add(new ProcessedRecord(data, System.currentTimeMillis())); // OOM eventually
}
// ✅ GOOD: Circular buffer with bounded size and periodic flush to storage
private static final Deque<ProcessedRecord> processedBuffer = new ArrayDeque<>(10_000);
public void processBounded(String data) {
if (processedBuffer.size() >= 10_000) {
flushToStorage(processedBuffer);
processedBuffer.clear();
}
processedBuffer.addLast(new ProcessedRecord(data, System.currentTimeMillis()));
}
// ── Root Cause: Lock Ordering Deadlock → Fix: Consistent Global Lock Order ───────
private final Object lockA = new Object();
private final Object lockB = new Object();
// ❌ BAD: Thread 1 acquires lockA→lockB, Thread 2 acquires lockB→lockA = DEADLOCK
public void transfer(int amount) {
synchronized (lockA) {
synchronized (lockB) { /* ... */ }
}
}
public void reverseTransfer(int amount) {
synchronized (lockB) { // Different order from above → deadlock potential
synchronized (lockA) { /* ... */ }
}
}
// ✅ GOOD: Always acquire locks by consistent global ordering (identity hash)
private static void synchronizedTogether(Object l1, Object l2, Runnable action) {
Object first = System.identityHashCode(l1) < System.identityHashCode(l2) ? l1 : l2;
Object second = first == l1 ? l2 : l1;
synchronized (first) {
synchronized (second) {
action.run();
}
}
}
// ── Root Cause: Thread Pool Starvation → Fix: Separate Executor for Blocking I/O ──
private final ExecutorService ioExecutor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2
);
// ✅ GOOD: Blocking I/O runs on dedicated executor, not shared application pool
public String handleRequest() throws Exception {
return ioExecutor.submit(() -> slowDatabaseQuery()).get(10, TimeUnit.SECONDS);
}
Validate Remediation — After applying fixes, validate the system under production-like load:
# Verify thread dump no longer shows BLOCKED threads accumulating
jcmd <pid> Thread.print | grep -c "BLOCKED"
# Verify heap usage remains stable (no monotonic growth) over 30+ minute sustained load
jstat -gcutil <pid> 10000 180 > post_fix_gc_stats.csv
# For OOM: verify heap dump shows dominant objects have been eliminated
# Use MAT to compare pre-fix and post-fix heap dumps if available
# For GC thrashing: verify GC log shows healthy collection patterns
grep -E "GC pause" /var/log/app/gc.log | tail -20
Checkpoint: Monitor the corrected metric for at least 30 minutes. Memory issues require longer observation (1-4 hours) to confirm leaks are actually fixed, as gradual growth may not be immediately apparent.
Thread dumps are the single most valuable diagnostic artifact for JVM incidents. Learn to read them quickly during production outages.
# Count threads in each state from a thread dump file
grep -c "RUNNABLE" threads.txt # Executing code
grep -c "BLOCKED" threads.txt # Waiting for monitor lock
grep -c "WAITING" threads.txt # Waiting indefinitely (LockSupport.park)
grep -c "TIMED_WAITING" threads.txt # Waiting with timeout (Thread.sleep, wait())
JVM thread dumps include automatic deadlock detection output. Look for this section:
Found one Java-level deadlock:
=============================
"pool-1-thread-4":
waiting to lock monitor 0x00007f8b1c003e68 (object 0x000000076ab2e4d8, a java.lang.Object),
which is held by "pool-1-thread-2"
"pool-1-thread-2":
waiting to lock monitor 0x00007f8b1c003f38 (object 0x000000076ab2e5a0, a java.lang.Object),
which is held by "pool-1-thread-4"
Java stack information for the threads listed above:
===================================================
"pool-1-thread-4":
at com.example.TransferService.transfer(TransferService.java:42)
- waiting to lock <0x000000076ab2e4d8> (a java.lang.Object)
- locked <0x000000076ab2e5a0> (a java.lang.Object)
at com.example.TransferService.reverseTransfer(TransferService.java:58)
"pool-1-thread-2":
at com.example.TransferService.reverseTransfer(TransferService.java:58)
- waiting to lock <0x000000076ab2e5a0> (a java.lang.Object)
- locked <0x000000076ab2e4d8> (a java.lang.Object)
at com.example.TransferService.transfer(TransferService.java:42)
"pool-1-thread-3" #15 prio=5 os_prio=0 cpu=123.4ms elapsed=3600000ms tid=0x00007f8b1c0a1000 nid=0x1234 waiting on condition [0x00007f8b14ffd000]
java.lang.Thread.State: WAITING (parking)
at jdk.internal.misc.Unsafe.park(java.base@21/Native Method)
- parking to wait for <0x000000076ab5f0a0> (a java.util.concurrent.FutureTask$InterruptibleCarry)
at java.util.concurrent.locks.LockSupport.park(java.base@21/LockSupport.java:221)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(java.base@21/AbstractQueuedSynchronizer.java:473)
at java.util.concurrent.FutureTask.get(java.base@21/FutureTask.java:205)
at com.example.Handler.processRequest(Handler.java:87)
Locked ownable synchronizers:
- None
This thread is stuck waiting for a Future result that may never arrive because all threads in the pool are also blocked. The key indicator: all pool threads show WAITING state, and some are waiting on each other's futures.
Eclipse Memory Analyzer (MAT) is the standard tool for analyzing Java heap dumps. Load the HPROF file and examine these reports in priority order:
This is the primary diagnostic output from MAT. It automatically identifies objects that are holding unexpectedly large amounts of retained heap memory. Key fields to examine:
The Dominator Tree shows objects in descending order of retained heap. Top entries reveal which classes consume the most memory:
Dominator Tree (sorted by retained size):
com.example.CacheEntry[] 1,245,678,901 45.2% [root]
java.lang.String 890,123,456 32.3% └─ cached by CacheEntry[].value
byte[] 234,567,890 8.5% ├─ internalized in String
com.example.UserContext 123,456,789 4.5% └─ retained by Thread-42
Identify which threads retain the most heap memory. Useful for detecting ThreadLocal leaks:
Thread Overviews:
"pool-1-thread-42" 123,456,789 bytes 4.5% (ThreadLocal/UserContext)
"main" 12,345,678 bytes 0.4%
"GC-task-threads" 1,234,567 bytes 0.04%
When the JVM crashes (native error, stack overflow, or internal error), it writes a hs_err_pid<pid>.log file in the working directory. Analyze this file in order:
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007f8b1c0a1000, pid=12345, tid=67890
#
# JRE version: OpenJDK 21.0.1+12 (21.0.1+12)
# Java VM: OpenJDK 64-Bit Server VM (21.0.1+12, mixed mode, sharing)
# Problematic frame:
# v ~BufferBlob::Interpreter frames
Key information:
SIGSEGV = segfault (native memory access violation), SIGBUS = bus error, SIGABRT = abortLook for these sections in order of priority:
# Stack: [0x00007f8b14ffd000,0x00007f8b150fe000], sp=0x00007f8b150fc9e0, free space=1016k
Native frames: (J=compiled java code, j=interpreted, Vv=VM code, C=native code)
C [libc.so.6+0x... ] __restore_rt
C [libjvm.so+0x... ] JVM_RegisterNatives+0x...
Java frames: (J=compiled java code, j=interpreted, Vv=VM code)
J 8945 com.example.RecursiveParser.parse(RecursiveParser.java:42) jvmti
J 8944 com.example.RecursiveParser.parseChildren(RecursiveParser.java:67) jvmti
Stack overflow manifests as a very small free stack space and deeply nested Java frames. Common causes: infinite recursion, excessive logging recursion, or toString() methods that trigger recursive calls on circular object graphs.
Modern JVMs (Java 9+) use unified logging for GC events. Parse GC logs to identify problematic patterns.
# Check if GC is running more than 90% of the time with minimal heap reclaimed:
grep "GC pause" /var/log/app/gc.log | awk '{print $1, $3}' | tail -100
# If you see collections every 1-5 seconds with "old gen" or "mixed" collections → thrashing
Example GC log analysis showing thrashing:
2024-03-15T10:23:01.123+0000: 123.456: [GC pause (G1 Evacuation Pause) (young) (metric) ... heap usage: 78% → 77%]
2024-03-15T10:23:01.456+0000: 123.789: [GC pause (G1 Evacuation Pause) (young) (metric) ... heap usage: 82% → 81%]
2024-03-15T10:23:01.789+0000: 124.122: [GC pause (G1 Evacuation Pause) (young) (metric) ... heap usage: 86% → 85%]
Note the heap usage is not decreasing — collections are running but memory is not being reclaimed. This confirms a memory leak rather than temporary GC pressure.
# Search for full GC (FGC) which causes longest pause times:
grep "Full GC" /var/log/app/gc.log | tail -20
# Full GCs should be extremely rare in production. Frequent FGC indicates:
# 1. Metaspace exhaustion (too many loaded classes)
# 2. Old generation filling faster than concurrent marking can keep up
# 3. Insufficient heap for the working set size
OutOfMemoryError type precisely: Java heap space indicates heap capacity issue or memory leak; Metaspace indicates too many loaded classes or classloader leaks; GC overhead limit exceeded indicates GC is spending > 98% of time collecting < 2% of heap reclaimedhs_err_pid*.log crash files for native memory issues (stack overflow, OOM in native memory, JNI errors) — these indicate JVM-level or OS-level problems that require different remediation than Java heap issues-XX:+UseConcMarkSweeper (CMS) in new deployments — it has been deprecated since JDK 9 and removed in JDK 14; use G1GC or ZGC insteadTIMED_WAITING threads in thread dump analysis — while some are normal (sleep, lock waits), a high proportion of TIMED_WAITING threads indicates application-level spin-waiting patterns that waste CPU without making progressWhen diagnosing a JVM incident, produce:
OutOfMemoryError, crash, hang), application context (JDK version, GC type, heap size, container limits), and timeline of symptoms observed| Resource | URL |
|---|---|
| JDK 21 JVM Diagnostic Tools (jcmd, jstat, jstack) | https://docs.oracle.com/en/java/javase/21/tools/jcmd.html |
| OpenJDK GC Log Analysis Guide | https://openjdk.org/groups/vm/garbage-collector.html |
| Eclipse Memory Analyzer Tool (MAT) User Guide | https://www.eclipse.org/mat/userguide.php |
| JVM Crash Log (hs_err_pid*.log) Reference | https://docs.oracle.com/en/java/javase/21/troubleshoot/reporting-jvm-crashes-and-errors-using-core-dumps008.html |
| Java Thread Dump Analysis Guide | https://docs.oracle.com/en/java/javase/21/management/thread-and-processor-monitoring.html |
| OpenJDK JVM Crash Analysis (hs_err_pid log) | https://wiki.openjdk.org/display/HotSpot/How+to+analyze+a+core+dump |
| JDK Diagnostic Command Reference | https://docs.oracle.com/en/java/javase/21/tools/jcmd.html |
| Skill | Purpose |
|---|---|
jvm-performance-tuning | Proactive JVM optimization (GC selection, memory configuration, JIT tuning) — use for pre-deployment tuning; this skill handles reactive troubleshooting when production incidents occur |
framework-performance-tuning | Framework-level debugging (connection pool exhaustion, framework-specific thread pools, caching issues) — many "JVM issues" actually originate from misconfigured frameworks like Spring or Hibernate |
async-programming | Virtual threads and structured concurrency diagnostics (JDK 21+) — use when the incident involves virtual thread behavior, uncaught exceptions in virtual threads, or ThreadFactory misconfiguration |
📖 skill(local cache): jvm-performance-tuning, framework-performance-tuning, async-programming