JVM-Performance-Deep-Dive-Lab

Ein zusammenhängender Diagnosepfad von korrelierten Messwerten über Evidenz und Hypothese bis zur Kontrollmessung. Die Codekarten werden direkt aus den kompilierbaren Java-Dateien erzeugt.

26 Java-Dateien · Java 21 · ohne externe Laufzeitabhängigkeiten

AllocationPressureSimulator

src/main/java/com/example/enterprise/run8a/AllocationPressureSimulator.java

Erzeugt kontrollierte kurzlebige Allokationen für Übungen.

package com.example.enterprise.run8a;

import java.util.ArrayList;
import java.util.List;

public final class AllocationPressureSimulator {
    public List<byte[]> allocateBursts(int objects, int bytesPerObject) {
        List<byte[]> data = new ArrayList<>();
        for (int index = 0; index < objects; index++) {
            byte[] value = new byte[bytesPerObject];
            value[0] = (byte) index;
            data.add(value);
        }
        return data;
    }

    public long checksum(List<byte[]> data) {
        long checksum = 0;
        for (byte[] value : data) {
            checksum += value[0];
        }
        return checksum;
    }
}

BenchmarkReport

src/main/java/com/example/enterprise/run8a/BenchmarkReport.java

Berechnet Median und p95, ohne sich als JMH-Ersatz auszugeben.

package com.example.enterprise.run8a;

import java.util.Arrays;

/** Descriptive statistics for a teaching measurement; not a replacement for JMH. */
public record BenchmarkReport(
        String operation,
        int warmupIterations,
        long[] measuredNanos) {

    public BenchmarkReport {
        measuredNanos = measuredNanos.clone();
        if (measuredNanos.length == 0) {
            throw new IllegalArgumentException("At least one measured iteration is required");
        }
    }

    @Override
    public long[] measuredNanos() {
        return measuredNanos.clone();
    }

    public long medianNanos() {
        return percentile(0.50);
    }

    public long p95Nanos() {
        return percentile(0.95);
    }

    public long percentile(double quantile) {
        if (quantile < 0.0 || quantile > 1.0) {
            throw new IllegalArgumentException("quantile must be between 0 and 1");
        }
        long[] sorted = measuredNanos.clone();
        Arrays.sort(sorted);
        int index = (int) Math.ceil(quantile * sorted.length) - 1;
        return sorted[Math.max(0, index)];
    }
}

ClassLoadingProbe

src/main/java/com/example/enterprise/run8a/ClassLoadingProbe.java

Erfasst aktuelle, insgesamt geladene und entladene Klassen.

package com.example.enterprise.run8a;

import java.lang.management.ManagementFactory;

public final class ClassLoadingProbe {
    public ClassLoadingSnapshot capture() {
        var bean = ManagementFactory.getClassLoadingMXBean();
        return new ClassLoadingSnapshot(
                bean.getLoadedClassCount(),
                bean.getTotalLoadedClassCount(),
                bean.getUnloadedClassCount(),
                ClassLoader.getSystemClassLoader());
    }
}

ClassLoadingSnapshot

src/main/java/com/example/enterprise/run8a/ClassLoadingSnapshot.java

Hält Classloading-Daten und den zugehörigen Application ClassLoader.

package com.example.enterprise.run8a;

public record ClassLoadingSnapshot(
        int loadedNow,
        long totalLoaded,
        long unloaded,
        ClassLoader appLoader) {}

ContainerMemorySnapshot

src/main/java/com/example/enterprise/run8a/ContainerMemorySnapshot.java

Trennt Heap-Auslastung von RSS- und Native-Memory-Druck.

package com.example.enterprise.run8a;

/** Memory view used to distinguish Java heap pressure from container pressure. */
public record ContainerMemorySnapshot(
        long limitBytes,
        long processResidentBytes,
        long directBufferBytes,
        long estimatedThreadStackBytes) {

    public long nativeAndUntrackedBytes(MemorySnapshot memory) {
        long explained = memory.heapUsed() + memory.nonHeapUsed() + directBufferBytes
                + estimatedThreadStackBytes;
        return Math.max(0, processResidentBytes - explained);
    }

    public double processUtilization() {
        return limitBytes > 0 ? (double) processResidentBytes / limitBytes : 0.0;
    }

    public boolean closeToLimit() {
        return limitBytes > 0 && processUtilization() >= 0.90;
    }
}

DiagnosticScenarioFactory

src/main/java/com/example/enterprise/run8a/DiagnosticScenarioFactory.java

Liefert reproduzierbare Vorher-/Nachher-Snapshots für das Allocation-Szenario.

package com.example.enterprise.run8a;

import java.util.List;
import java.util.Map;

/** Reproducible snapshots make the decision table understandable and testable. */
public final class DiagnosticScenarioFactory {
    private static final long MIB = 1024L * 1024L;

    public JvmDiagnosticSnapshot allocationIncident() {
        return snapshot(
                new MemorySnapshot(900 * MIB, 960 * MIB, 1024 * MIB, 140 * MIB, 1),
                Map.of(Thread.State.RUNNABLE, 8, Thread.State.WAITING, 2),
                0.68,
                240.0,
                340.0,
                new ContainerMemorySnapshot(2_048 * MIB, 1_300 * MIB, 80 * MIB, 40 * MIB));
    }

    public JvmDiagnosticSnapshot afterCacheBounded() {
        return snapshot(
                new MemorySnapshot(520 * MIB, 768 * MIB, 1024 * MIB, 135 * MIB, 2),
                Map.of(Thread.State.RUNNABLE, 8, Thread.State.WAITING, 2),
                0.42,
                55.0,
                170.0,
                new ContainerMemorySnapshot(2_048 * MIB, 850 * MIB, 55 * MIB, 40 * MIB));
    }

    private JvmDiagnosticSnapshot snapshot(
            MemorySnapshot memory,
            Map<Thread.State, Integer> threadStates,
            double cpu,
            double allocationRate,
            double p95,
            ContainerMemorySnapshot container) {
        return new JvmDiagnosticSnapshot(
                memory,
                new ThreadStateSnapshot(threadStates),
                List.of(new GarbageCollectorSnapshot("G1 Young", 14, 380)),
                new ClassLoadingSnapshot(12_000, 13_400, 1_400,
                        ClassLoader.getSystemClassLoader()),
                container,
                cpu,
                allocationRate,
                p95);
    }
}

GarbageCollectorProbe

src/main/java/com/example/enterprise/run8a/GarbageCollectorProbe.java

Erfasst Collector-Zähler und kumulierte Laufzeiten.

package com.example.enterprise.run8a;

import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.List;

public final class GarbageCollectorProbe {
    public List<GarbageCollectorSnapshot> capture() {
        List<GarbageCollectorSnapshot> snapshots = new ArrayList<>();
        for (GarbageCollectorMXBean bean : ManagementFactory.getGarbageCollectorMXBeans()) {
            snapshots.add(new GarbageCollectorSnapshot(
                    bean.getName(),
                    bean.getCollectionCount(),
                    bean.getCollectionTime()));
        }
        return List.copyOf(snapshots);
    }
}

GarbageCollectorSnapshot

src/main/java/com/example/enterprise/run8a/GarbageCollectorSnapshot.java

Hält die stabilen GC-Messwerte eines Collectors.

package com.example.enterprise.run8a;

public record GarbageCollectorSnapshot(
        String name,
        long collectionCount,
        long collectionTimeMillis) {}

GcLogLine

src/main/java/com/example/enterprise/run8a/GcLogLine.java

Berechnet zurückgewonnenen Speicher und erkennt geringe Rückgewinnung.

package com.example.enterprise.run8a;

public record GcLogLine(
        String collector,
        long beforeMb,
        long afterMb,
        long committedMb,
        long pauseMillis) {

    public long reclaimedMb() {
        return beforeMb - afterMb;
    }

    public boolean suspiciousNoReclaim() {
        return beforeMb > 0 && reclaimedMb() < beforeMb * 0.05;
    }
}

GcLogParser

src/main/java/com/example/enterprise/run8a/GcLogParser.java

Liest Lehrformat und einen realistischen Ausschnitt des Unified GC Loggings.

package com.example.enterprise.run8a;

import java.util.Optional;
import java.util.regex.Pattern;

/** Parses a stable teaching format and a useful subset of Java unified GC logs. */
public final class GcLogParser {
    private static final Pattern TEACHING_FORMAT = Pattern.compile(
            "GC\\(([^)]+)\\) before=(\\d+)M after=(\\d+)M committed=(\\d+)M pause=([0-9.]+)ms");

    private static final Pattern UNIFIED_LOG = Pattern.compile(
            "GC\\(\\d+\\)\\s+(.+?)\\s+(\\d+)M->(\\d+)M\\((\\d+)M\\)\\s+([0-9.]+)ms");

    public Optional<GcLogLine> parse(String line) {
        var teaching = TEACHING_FORMAT.matcher(line);
        if (teaching.find()) {
            return Optional.of(toLine(teaching.group(1), teaching.group(2), teaching.group(3),
                    teaching.group(4), teaching.group(5)));
        }

        var unified = UNIFIED_LOG.matcher(line);
        if (unified.find()) {
            return Optional.of(toLine(unified.group(1).trim(), unified.group(2), unified.group(3),
                    unified.group(4), unified.group(5)));
        }
        return Optional.empty();
    }

    private GcLogLine toLine(
            String collectorOrCause,
            String before,
            String after,
            String committed,
            String pauseMillis) {
        return new GcLogLine(
                collectorOrCause,
                Long.parseLong(before),
                Long.parseLong(after),
                Long.parseLong(committed),
                Math.round(Double.parseDouble(pauseMillis)));
    }
}

HotPathAnalyzer

src/main/java/com/example/enterprise/run8a/HotPathAnalyzer.java

Sammelt Messungen und erzeugt einen deskriptiven Benchmark-Bericht.

package com.example.enterprise.run8a;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

/** Aggregates local timing probes. Use JFR/JMH when measurement quality matters. */
public final class HotPathAnalyzer {
    private final List<ProfilingSample> samples = new ArrayList<>();

    public void record(String name, long nanos) {
        samples.add(new ProfilingSample(name, nanos));
    }

    public List<ProfilingSample> samples() {
        return List.copyOf(samples);
    }

    public Optional<ProfilingSample> slowest() {
        return samples.stream()
                .max(Comparator.comparingLong(ProfilingSample::durationNanos));
    }

    public BenchmarkReport report(String operation, int warmupIterations) {
        long[] values = samples.stream()
                .filter(sample -> sample.name().equals(operation))
                .mapToLong(ProfilingSample::durationNanos)
                .toArray();
        return new BenchmarkReport(operation, warmupIterations, values);
    }
}

IncidentAssessment

src/main/java/com/example/enterprise/run8a/IncidentAssessment.java

Transportiert Diagnose, beobachtete Evidenz und empfohlene Vertiefung.

package com.example.enterprise.run8a;

import java.util.List;

/** Diagnosis result with evidence and a next action instead of a context-free label. */
public record IncidentAssessment(
        IncidentQuestion primaryDiagnosis,
        List<String> evidence,
        List<String> nextActions) {

    public IncidentAssessment {
        evidence = List.copyOf(evidence);
        nextActions = List.copyOf(nextActions);
    }
}

IncidentQuestion

src/main/java/com/example/enterprise/run8a/IncidentQuestion.java

Benennt die sechs Diagnosepfade plus unbekannte Ursache.

package com.example.enterprise.run8a;

public enum IncidentQuestion {
    CPU_BOUND,
    ALLOCATION_BOUND,
    LOCK_BOUND,
    IO_BOUND,
    CLASSLOADING_BOUND,
    CONTAINER_LIMIT_BOUND,
    UNKNOWN
}

JvmDiagnosticSnapshot

src/main/java/com/example/enterprise/run8a/JvmDiagnosticSnapshot.java

Korreliert Heap, Threads, GC, Classloading, CPU, Container und Latenz.

package com.example.enterprise.run8a;

import java.util.List;
import java.util.Objects;

/** One correlated point in time. Values from different incidents must not be mixed. */
public record JvmDiagnosticSnapshot(
        MemorySnapshot memory,
        ThreadStateSnapshot threads,
        List<GarbageCollectorSnapshot> garbageCollectors,
        ClassLoadingSnapshot classLoading,
        ContainerMemorySnapshot container,
        double processCpuLoad,
        double allocationRateMbPerSecond,
        double requestP95Millis) {

    public JvmDiagnosticSnapshot {
        Objects.requireNonNull(memory, "memory");
        Objects.requireNonNull(threads, "threads");
        garbageCollectors = List.copyOf(garbageCollectors);
        Objects.requireNonNull(classLoading, "classLoading");
        Objects.requireNonNull(container, "container");
    }

    public long totalCollections() {
        return garbageCollectors.stream()
                .mapToLong(GarbageCollectorSnapshot::collectionCount)
                .filter(value -> value > 0)
                .sum();
    }

    public long totalCollectionTimeMillis() {
        return garbageCollectors.stream()
                .mapToLong(GarbageCollectorSnapshot::collectionTimeMillis)
                .filter(value -> value > 0)
                .sum();
    }
}

JvmIncidentClassifier

src/main/java/com/example/enterprise/run8a/JvmIncidentClassifier.java

Ordnet Messwerte einer prüfbaren Hypothese mit Evidenz und nächsten Schritten zu.

package com.example.enterprise.run8a;

import java.util.ArrayList;
import java.util.List;

/**
 * Small, explicit decision table. It does not claim certainty: it turns correlated
 * observations into the next falsifiable diagnostic step.
 */
public final class JvmIncidentClassifier {

    public IncidentAssessment assess(JvmDiagnosticSnapshot snapshot) {
        List<String> evidence = new ArrayList<>();
        List<String> actions = new ArrayList<>();

        if (snapshot.container().closeToLimit()
                && !snapshot.memory().heapLooksTight()) {
            evidence.add("Process uses at least 90% of the container limit while heap is below 85%.");
            evidence.add("Untracked/native estimate: "
                    + snapshot.container().nativeAndUntrackedBytes(snapshot.memory()) + " bytes.");
            actions.add("Enable Native Memory Tracking and inspect direct buffers, stacks and metaspace.");
            actions.add("Compare -Xmx plus native headroom with the Kubernetes memory limit.");
            return new IncidentAssessment(IncidentQuestion.CONTAINER_LIMIT_BOUND, evidence, actions);
        }

        if (snapshot.threads().blockedRatio() >= 0.25
                || snapshot.threads().count(Thread.State.BLOCKED) >= 4) {
            evidence.add("Blocked threads: " + snapshot.threads().count(Thread.State.BLOCKED)
                    + " of " + snapshot.threads().totalThreads() + '.');
            actions.add("Capture two thread dumps and compare lock owners and waiters.");
            actions.add("Inspect synchronized regions before increasing thread counts.");
            return new IncidentAssessment(IncidentQuestion.LOCK_BOUND, evidence, actions);
        }

        if (snapshot.memory().heapLooksTight()
                && snapshot.allocationRateMbPerSecond() >= 100.0) {
            evidence.add("Heap utilization: " + percent(snapshot.memory().heapUtilization()) + '.');
            evidence.add("Allocation rate: " + snapshot.allocationRateMbPerSecond() + " MiB/s.");
            actions.add("Record JFR allocation events and inspect top allocating stack traces.");
            actions.add("Compare live-set size after GC before changing heap or collector settings.");
            return new IncidentAssessment(IncidentQuestion.ALLOCATION_BOUND, evidence, actions);
        }

        if (snapshot.processCpuLoad() >= 0.85
                && snapshot.threads().blockedRatio() < 0.10) {
            evidence.add("Process CPU load: " + percent(snapshot.processCpuLoad()) + '.');
            evidence.add("Few threads are blocked, so lock contention is not the first hypothesis.");
            actions.add("Capture a JFR execution profile or async-profiler flame graph.");
            actions.add("Validate the hot path with representative production-like input.");
            return new IncidentAssessment(IncidentQuestion.CPU_BOUND, evidence, actions);
        }

        if (snapshot.requestP95Millis() >= 500.0
                && snapshot.processCpuLoad() < 0.50
                && snapshot.threads().count(Thread.State.WAITING)
                        + snapshot.threads().count(Thread.State.TIMED_WAITING) >= 4) {
            evidence.add("Request p95 is high while process CPU is below 50%.");
            evidence.add("Many threads are waiting rather than executing.");
            actions.add("Correlate traces with database, HTTP client and queue wait times.");
            actions.add("Check timeouts and connection-pool saturation before adding threads.");
            return new IncidentAssessment(IncidentQuestion.IO_BOUND, evidence, actions);
        }

        if (snapshot.classLoading().loadedNow() >= 50_000
                || snapshot.classLoading().totalLoaded() - snapshot.classLoading().unloaded() >= 75_000) {
            evidence.add("Unusually high number of currently or historically loaded classes.");
            actions.add("Inspect class-loader statistics and repeated deployment/plugin loaders.");
            actions.add("Correlate class count with metaspace growth over time.");
            return new IncidentAssessment(IncidentQuestion.CLASSLOADING_BOUND, evidence, actions);
        }

        evidence.add("No threshold in the teaching decision table is crossed.");
        actions.add("Collect a longer time series and align JVM data with request metrics and deploys.");
        return new IncidentAssessment(IncidentQuestion.UNKNOWN, evidence, actions);
    }

    /** Compatibility entry point used by older exercises. */
    public IncidentQuestion classify(
            MemorySnapshot memory,
            ThreadStateSnapshot threads,
            List<GarbageCollectorSnapshot> garbageCollectors) {
        var snapshot = new JvmDiagnosticSnapshot(
                memory,
                threads,
                garbageCollectors,
                new ClassLoadingSnapshot(0, 0, 0, ClassLoader.getSystemClassLoader()),
                new ContainerMemorySnapshot(0, 0, 0, 0),
                0.0,
                memory.heapLooksTight() ? 150.0 : 0.0,
                0.0);
        return assess(snapshot).primaryDiagnosis();
    }

    private String percent(double ratio) {
        return "%.1f%%".formatted(ratio * 100.0);
    }
}

LeakProneStaticRegistry

src/main/java/com/example/enterprise/run8a/LeakProneStaticRegistry.java

Zeigt starke statische Retention als bewusstes Anti-Pattern.

package com.example.enterprise.run8a;

import java.util.ArrayList;
import java.util.List;

// Anti-Pattern: A static registry becomes a GC root and retains every value.
public final class LeakProneStaticRegistry {
    private static final List<byte[]> RETAINED = new ArrayList<>();

    private LeakProneStaticRegistry() {}

    public static void retain(byte[] data) {
        RETAINED.add(data);
    }

    public static int retainedCount() {
        return RETAINED.size();
    }

    public static void clear() {
        RETAINED.clear();
    }
}

MemoryProbe

src/main/java/com/example/enterprise/run8a/MemoryProbe.java

Liest Heap und Non-Heap über die standardisierten MXBeans.

package com.example.enterprise.run8a;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;

public final class MemoryProbe {
    private final MemoryMXBean bean = ManagementFactory.getMemoryMXBean();

    public MemorySnapshot capture() {
        var heap = bean.getHeapMemoryUsage();
        var nonHeap = bean.getNonHeapMemoryUsage();
        return new MemorySnapshot(
                heap.getUsed(),
                heap.getCommitted(),
                heap.getMax(),
                nonHeap.getUsed(),
                System.nanoTime());
    }
}

MemorySnapshot

src/main/java/com/example/enterprise/run8a/MemorySnapshot.java

Berechnet freie Commit-Kapazität und Heap-Auslastung.

package com.example.enterprise.run8a;

public record MemorySnapshot(
        long heapUsed,
        long heapCommitted,
        long heapMax,
        long nonHeapUsed,
        long timestampNanos) {

    public long heapFreeInsideCommit() {
        return Math.max(0, heapCommitted - heapUsed);
    }

    public double heapUtilization() {
        return heapMax > 0 ? (double) heapUsed / heapMax : 0.0;
    }

    public boolean heapLooksTight() {
        return heapMax > 0 && heapUtilization() >= 0.85;
    }
}

ProfilingSample

src/main/java/com/example/enterprise/run8a/ProfilingSample.java

Repräsentiert eine einzelne lokale Zeitmessung.

package com.example.enterprise.run8a;

public record ProfilingSample(String name, long durationNanos) {
    public long millis() {
        return durationNanos / 1_000_000;
    }
}

ProfilingTimer

src/main/java/com/example/enterprise/run8a/ProfilingTimer.java

Instrumentiert einen lokalen Codebereich mit monotonic time.

package com.example.enterprise.run8a;

// Scope Timer: instruments a local block without adding a framework dependency.
public final class ProfilingTimer implements AutoCloseable {
    private final String name;
    private final HotPathAnalyzer analyzer;
    private final long startNanos;

    public ProfilingTimer(String name, HotPathAnalyzer analyzer) {
        this.name = name;
        this.analyzer = analyzer;
        this.startNanos = System.nanoTime();
    }

    @Override
    public void close() {
        analyzer.record(name, System.nanoTime() - startNanos);
    }
}

RetainedCache

src/main/java/com/example/enterprise/run8a/RetainedCache.java

Demonstriert einen begrenzten, zugriffsbasierten LRU-Cache.

package com.example.enterprise.run8a;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

// Bounded LRU cache: prevents unlimited retention in the teaching scenario.
public final class RetainedCache<K, V> {
    private final int maxEntries;
    private final LinkedHashMap<K, V> map;

    public RetainedCache(int maxEntries) {
        if (maxEntries < 1) {
            throw new IllegalArgumentException("maxEntries must be positive");
        }
        this.maxEntries = maxEntries;
        this.map = new LinkedHashMap<>(16, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
                return size() > RetainedCache.this.maxEntries;
            }
        };
    }

    public synchronized void put(K key, V value) {
        map.put(key, value);
    }

    public synchronized V get(K key) {
        return map.get(key);
    }

    public synchronized int size() {
        return map.size();
    }

    public synchronized Set<K> keys() {
        return new LinkedHashSet<>(map.keySet());
    }
}

Run8ADemo

src/main/java/com/example/enterprise/run8a/Run8ADemo.java

Führt als Einstieg durch Incident, Evidenz, Maßnahme und Kontrollmessung.

package com.example.enterprise.run8a;

/** Walks through one incident instead of printing unrelated probe values. */
public final class Run8ADemo {
    public static void main(String[] args) {
        var scenarios = new DiagnosticScenarioFactory();
        var classifier = new JvmIncidentClassifier();

        IncidentAssessment before = classifier.assess(scenarios.allocationIncident());
        IncidentAssessment after = classifier.assess(scenarios.afterCacheBounded());

        var gcEvent = new GcLogParser().parse(
                "[2.345s][info][gc] GC(12) Pause Young (Normal) 512M->128M(1024M) 12.345ms")
                .orElseThrow();

        System.out.println("Incident before: " + before.primaryDiagnosis());
        before.evidence().forEach(item -> System.out.println("  evidence: " + item));
        before.nextActions().forEach(item -> System.out.println("  next: " + item));
        System.out.println("After bounded cache: " + after.primaryDiagnosis());
        System.out.println("Parsed GC event: reclaimed=" + gcEvent.reclaimedMb()
                + " MiB, pause=" + gcEvent.pauseMillis() + " ms");
        System.out.println("RUN8A_DEMO_OK");
    }
}

Run8ATestRunner

src/main/java/com/example/enterprise/run8a/Run8ATestRunner.java

Prüft Parser, Cache, Perzentile und jeden Zweig der Decision Table.

package com.example.enterprise.run8a;

import java.util.List;
import java.util.Map;

/** Dependency-free executable tests so the lab also works without a test framework. */
public final class Run8ATestRunner {
    private static final long MIB = 1024L * 1024L;

    public static void main(String[] args) {
        boundedCacheEvictsLeastRecentlyUsedEntry();
        staticRegistryDemonstratesRetention();
        unifiedGcLogIsParsed();
        malformedGcLogIsRejected();
        benchmarkReportCalculatesPercentiles();
        allIncidentBranchesAreReachable();
        scenarioShowsImprovementAfterBoundingCache();
        System.out.println("RUN8A_TESTS_OK");
    }

    static void boundedCacheEvictsLeastRecentlyUsedEntry() {
        var cache = new RetainedCache<String, String>(2);
        cache.put("a", "1");
        cache.put("b", "2");
        cache.get("a");
        cache.put("c", "3");
        equals(2, cache.size());
        check(cache.keys().contains("a"));
        check(!cache.keys().contains("b"));
    }

    static void staticRegistryDemonstratesRetention() {
        LeakProneStaticRegistry.clear();
        LeakProneStaticRegistry.retain(new byte[8]);
        equals(1, LeakProneStaticRegistry.retainedCount());
        LeakProneStaticRegistry.clear();
        equals(0, LeakProneStaticRegistry.retainedCount());
    }

    static void unifiedGcLogIsParsed() {
        var parsed = new GcLogParser().parse(
                "[2.345s][info][gc] GC(12) Pause Young (Normal) 512M->128M(1024M) 12.345ms")
                .orElseThrow();
        equals(384L, parsed.reclaimedMb());
        equals(12L, parsed.pauseMillis());
    }

    static void malformedGcLogIsRejected() {
        check(new GcLogParser().parse("not a GC event").isEmpty());
    }

    static void benchmarkReportCalculatesPercentiles() {
        var report = new BenchmarkReport("parse", 5, new long[]{10, 20, 30, 40, 100});
        equals(30L, report.medianNanos());
        equals(100L, report.p95Nanos());
        long[] defensiveCopy = report.measuredNanos();
        defensiveCopy[0] = 999;
        equals(10L, report.measuredNanos()[0]);
    }

    static void allIncidentBranchesAreReachable() {
        equals(IncidentQuestion.ALLOCATION_BOUND,
                classify(snapshot(0.90, 0.60, 200, 100, states(Thread.State.RUNNABLE, 8), 10_000, false)));
        equals(IncidentQuestion.LOCK_BOUND,
                classify(snapshot(0.50, 0.30, 10, 300, states(Thread.State.BLOCKED, 4), 10_000, false)));
        equals(IncidentQuestion.CPU_BOUND,
                classify(snapshot(0.50, 0.92, 10, 200, states(Thread.State.RUNNABLE, 8), 10_000, false)));
        equals(IncidentQuestion.IO_BOUND,
                classify(snapshot(0.50, 0.20, 10, 900, states(Thread.State.WAITING, 6), 10_000, false)));
        equals(IncidentQuestion.CLASSLOADING_BOUND,
                classify(snapshot(0.50, 0.20, 10, 100, states(Thread.State.RUNNABLE, 4), 60_000, false)));
        equals(IncidentQuestion.CONTAINER_LIMIT_BOUND,
                classify(snapshot(0.50, 0.20, 10, 100, states(Thread.State.RUNNABLE, 4), 10_000, true)));
        equals(IncidentQuestion.UNKNOWN,
                classify(snapshot(0.50, 0.20, 10, 100, states(Thread.State.RUNNABLE, 4), 10_000, false)));
    }

    static void scenarioShowsImprovementAfterBoundingCache() {
        var factory = new DiagnosticScenarioFactory();
        equals(IncidentQuestion.ALLOCATION_BOUND, classify(factory.allocationIncident()));
        equals(IncidentQuestion.UNKNOWN, classify(factory.afterCacheBounded()));
    }

    private static JvmDiagnosticSnapshot snapshot(
            double heapRatio,
            double cpu,
            double allocationRate,
            double p95,
            Map<Thread.State, Integer> states,
            int loadedClasses,
            boolean containerTight) {
        long heapMax = 1_000 * MIB;
        var memory = new MemorySnapshot((long) (heapMax * heapRatio), heapMax, heapMax, 100 * MIB, 1);
        var container = containerTight
                ? new ContainerMemorySnapshot(1_000 * MIB, 950 * MIB, 50 * MIB, 20 * MIB)
                : new ContainerMemorySnapshot(2_000 * MIB, 800 * MIB, 50 * MIB, 20 * MIB);
        return new JvmDiagnosticSnapshot(
                memory,
                new ThreadStateSnapshot(states),
                List.of(),
                new ClassLoadingSnapshot(loadedClasses, loadedClasses, 0,
                        ClassLoader.getSystemClassLoader()),
                container,
                cpu,
                allocationRate,
                p95);
    }

    private static Map<Thread.State, Integer> states(Thread.State state, int count) {
        return Map.of(state, count);
    }

    private static IncidentQuestion classify(JvmDiagnosticSnapshot snapshot) {
        return new JvmIncidentClassifier().assess(snapshot).primaryDiagnosis();
    }

    private static void equals(Object expected, Object actual) {
        if (!java.util.Objects.equals(expected, actual)) {
            throw new AssertionError(expected + " != " + actual);
        }
    }

    private static void check(boolean condition) {
        if (!condition) {
            throw new AssertionError("condition is false");
        }
    }
}

ThreadStateSampler

src/main/java/com/example/enterprise/run8a/ThreadStateSampler.java

Erfasst Threadzustände und fragt die JVM nach Deadlocks.

package com.example.enterprise.run8a;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.EnumMap;
import java.util.Map;

public final class ThreadStateSampler {
    private final ThreadMXBean bean = ManagementFactory.getThreadMXBean();

    public ThreadStateSnapshot capture() {
        Map<Thread.State, Integer> counts = new EnumMap<>(Thread.State.class);
        for (long id : bean.getAllThreadIds()) {
            ThreadInfo info = bean.getThreadInfo(id);
            if (info != null) {
                counts.merge(info.getThreadState(), 1, Integer::sum);
            }
        }
        return new ThreadStateSnapshot(counts);
    }

    public long[] findDeadlockedThreadIds() {
        long[] ids = bean.findDeadlockedThreads();
        return ids == null ? new long[0] : ids.clone();
    }
}

ThreadStateSnapshot

src/main/java/com/example/enterprise/run8a/ThreadStateSnapshot.java

Verdichtet Threadzustände und berechnet den Blocked-Anteil.

package com.example.enterprise.run8a;

import java.util.EnumMap;
import java.util.Map;

public record ThreadStateSnapshot(Map<Thread.State, Integer> counts) {
    public ThreadStateSnapshot {
        EnumMap<Thread.State, Integer> copy = new EnumMap<>(Thread.State.class);
        copy.putAll(counts);
        counts = Map.copyOf(copy);
    }

    public int count(Thread.State state) {
        return counts.getOrDefault(state, 0);
    }

    public int totalThreads() {
        return counts.values().stream().mapToInt(Integer::intValue).sum();
    }

    public double blockedRatio() {
        return totalThreads() == 0 ? 0.0 : (double) count(Thread.State.BLOCKED) / totalThreads();
    }
}

WeakSessionRegistry

src/main/java/com/example/enterprise/run8a/WeakSessionRegistry.java

Zeigt schwache Referenzen für Daten ohne fachliche Ownership.

package com.example.enterprise.run8a;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

// Weak references are appropriate only when the registry does not own the values.
public final class WeakSessionRegistry {
    private final Map<String, WeakReference<byte[]>> sessions = new HashMap<>();

    public void remember(String id, byte[] data) {
        sessions.put(id, new WeakReference<>(data));
    }

    public int knownIds() {
        return sessions.size();
    }

    public int liveValues() {
        int liveValues = 0;
        for (var reference : sessions.values()) {
            if (reference.get() != null) {
                liveValues++;
            }
        }
        return liveValues;
    }
}
⌂ Cockpit