Skip to content

Architecture

How DIXY sees a deadlock.

Java App → Java agent / JVMTI → JFR and event collection → event processing → wait-for graph → deadlock detection → diagnostic output.

Target JVM

Collection

Processing

Detection

Output

Cycle detection

DeadlockDetectorAvailable

Depth-first search for cycles. PinningDetector keeps cycles containing a PINS edge.

Component interaction

Detection happens inside your JVM.

The agent keeps its own wait-for graph in the target process, so a cycle is found as soon as the last edge appears. The backend stores telemetry and reports for the CLI and API.

In the target JVM

  • VirtualDeadlockAgent, collectors, EventSender
  • GraphGateway + WaitForGraph + DeadlockDetector
  • RemoteReportSink (no Spring, no Jackson — fits the slim agent jar)

In the DIXY application

  • REST controllers with X-API-Key security (port 8081)
  • PostgreSQL persistence (Flyway) and Redis
  • Spring Shell commands and the dixy detect scanner

Event pipeline

One event, five steps.

  1. 01

    Collect

    JVMTI callbacks, the JFR stream and the thread-state poller each turn JVM activity into an EventDTO.

  2. 02

    Send

    EventSender hands every event to the in-process GraphGateway, then posts it to /api/telemetry without blocking.

  3. 03

    Apply

    GraphGateway updates owner and waiter registries under a ReentrantLock — a lock that parks, so it creates no monitor events of its own.

  4. 04

    Detect

    DeadlockDetector runs a DFS on the WaitForGraph after each update. A cycle becomes a DeadlockCycle.

  5. 05

    Report

    RemoteReportSink posts the cycle to /api/internal/deadlocks, with a 30-second cooldown per cycle to avoid duplicates.

Wait-for graph

A worked example.

Thread T1 holds lock A and wants lock B. Thread T2 holds lock B and wants lock A.

Worked example: T1 and T2 form a deadlock cycle through locks A and BholdswaitsholdswaitsT1T2AB
  • thread
  • lock
  • holds
  • waits
  • deadlock cycle
Equivalent Java
// T1
synchronized (a) {
synchronized (b) { … } // waits: T2 holds b
}
 
// T2
synchronized (b) {
synchronized (a) { … } // waits: T1 holds a
}

Collapsing the lock nodes gives the thread-only view the detector walks: T1 → T2 → T1. Any path that returns to its start is a deadlock.

Detection workflow

Watch a deadlock form, step by step.

Use Play, the arrows, or the step markers. With reduced motion enabled, Play jumps to the final step.

Wait-for graph, step 1: Two threads, two locksT1T2AB
  • thread
  • lock
  • holds
  • waits
  • deadlock cycle

Step 1 / 6

Two threads, two locks

T1 and T2 are running. Nobody owns lock A or lock B yet. The graph has nodes but no edges.

JVM integration notes

Details that keep reports honest.

JFR JavaMonitorEnter is history

JFR emits it after the monitor is acquired, so DIXY treats it as "wait is over" instead of a live edge. This removed false cycles.

Waits without an owner are ignored

A blocked event with no known owner (e.g. Object.wait) cannot be part of a cycle, so it adds no edge.

No self-monitoring by default

Attaching JVMTI hooks to DIXY's own JVM caused a recursive feedback loop; the agent targets a separate JVM.

Platform coverage

The native library is bundled for Linux x86-64 and Windows x86-64. macOS and ARM need a custom build.

More in the docs: JVM Diagnostics · Deadlock Detection