JVM Inspector with AI: Build a Smart Diagnostic Tool with Quarkus, LangChain4j, and Dev Services
Combine the power of Java introspection tools with a local AI model to analyze running JVMs in plain English.
Most observability tools speak machine. But what if you could ask your system a human question like:
“Show me all running JVMs. Is anything stuck or deadlocked?”
In this tutorial, you’ll build exactly that.
We’ll use:
Quarkus for building a fast, developer-friendly backend.
LangChain4j to integrate a local Large Language Model (LLM) using the
@Tool
interface.Dev Services to run Ollama locally without needing Docker commands.
A real JVM inspection utility that lists Java processes and performs thread dumps.
This is also a great MCP (Model Context Protocol) use case. MCP allows tools and models to expose structured capabilities. Here, you expose JVM diagnostics as structured tools the model can invoke deterministically. This is the foundation of reliable AI agents.
Step 1: Project Setup
Generate a Quarkus project with the following:
Group:
org.acme
Artifact:
jvm-inspector-ai
Java: 17+
Extensions:
quarkus-langchain4j-ollama
quarkus-rest
Create it at code.quarkus.io or with the CLI:
quarkus create app org.acme:jvm-inspector-ai --no-code \
--extension=langchain4j-ollama,rest
cd jvm-inspector-ai
And as usual, you can check out the complete project on my Github repository.
Step 2: Dev Services for Ollama
Quarkus Dev Services automates the startup of local services like Ollama by leveraging Testcontainers behind the scenes. When you include the quarkus-langchain4j-ollama
extension in your project and run in development mode (quarkus:dev
), Quarkus automatically detects the need for an Ollama instance. It then uses Testcontainers to find a local Podman or Docker installation, pull the required Ollama image, and start a container. It also injects the container's dynamic address and port into your application's configuration, so it connects seamlessly without any manual setup from you. This creates a "zero-config" experience, but it does rely on having a container engine installed on your machine.
In src/main/resources/application.properties
:
quarkus.langchain4j.ollama.chat-model.model-id=qwen3:latest
quarkus.langchain4j.ollama.timeout=120s
Qwen is a strong choice for building local AI agents, especially when tool integration and performance matter. The models use a mixture-of-experts (MoE) architecture, so you get large-model quality with a much smaller compute footprint
Step 3: JVM Tools Using @Tool
Let’s create a JvmTools
class. The JvmTools class is a utility class that provides JVM monitoring and diagnostic capabilities through LangChain4J tool annotations.
package org.acme;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.List;
import java.util.stream.Collectors;
import dev.langchain4j.agent.tool.Tool;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class JvmTools {
record JvmProcess(String processId, String displayName) {
}
@Tool("Displays all the JVM processes available on the current host")
public List<JvmProcess> getJvmProcesses() {
return ProcessHandle.allProcesses()
.filter(p -> p.info().command().map(cmd -> cmd.contains("java")).orElse(false))
.map(p -> new JvmProcess(String.valueOf(p.pid()),
p.info().commandLine().orElse("unknown")))
.collect(Collectors.toList());
}
@Tool("Performs a thread dump on a specific JVM process")
public String threadDump(String processId) {
long currentPid = ProcessHandle.current().pid();
if (!String.valueOf(currentPid).equals(processId)) {
return "Only the current process can be inspected in this example.";
}
ThreadMXBean bean = ManagementFactory.getThreadMXBean();
StringBuilder dump = new StringBuilder();
for (ThreadInfo ti : bean.dumpAllThreads(true, true)) {
dump.append(ti.toString());
}
return dump.toString();
}
}
It offers two main functions:
getJvmProcesses() - Discovers and lists all Java processes running on the current host, returning their process IDs and command line information as JvmProcess records.
threadDump(String processId) - Generates a thread dump for a specified JVM process. However, it's currently limited to only inspect the current process (for security/permission reasons in this example implementation).
Step 4: Simulated JVM Workloads
Create two classes: one for a long sleep thread, and one that simulates a deadlock. These give our thread dump something meaningful to show.
package org.acme;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.runtime.Startup;
@ApplicationScoped
@Startup
public class LongRunningTask {
@PostConstruct
void run() {
new Thread(() -> {
try {
Thread.sleep(300_000);
} catch (InterruptedException ignored) {}
}, "sleeping-thread").start();
}
}
package org.acme;
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.runtime.Startup;
@ApplicationScoped
@Startup
public class DeadlockSimulator {
private final Object a = new Object();
private final Object b = new Object();
@PostConstruct
void init() {
new Thread(() -> {
synchronized (a) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
synchronized (b) {
}
}
}, "deadlock-1").start();
new Thread(() -> {
synchronized (b) {
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {
}
synchronized (a) {
}
}
}, "deadlock-2").start();
}
}
Step 5: AI Service with LangChain4j
The JvmInspector interface defines an AI service for JVM inspection and diagnostics.
package org.acme;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.Tools;
import dev.langchain4j.service.UserMessage;
import io.quarkus.langchain4j.RegisterAiService;
@RegisterAiService(tools = JvmTools.class)
public interface JvmInspector {
@SystemMessage("""
You are an AI JVM inspector. You can list JVMs and run thread dumps.
Use the tools provided. Never fabricate PIDs or results.
""")
String chat(@UserMessage String userMessage);
}
Here's what it does:
AI Service Registration - The @RegisterAiService(tools = JvmTools.class) annotation registers this as a LangChain4J AI service that has access to the JVM inspection tools from the JvmTools class.
System Instructions - The @SystemMessage provides the AI with specific instructions about its role as a "JVM inspector" that can list JVMs and run thread dumps, with explicit guidance to use the provided tools and never fabricate results.
Chat Interface - The chat() method provides a conversational interface where users can send natural language messages about JVM inspection tasks, and the AI will respond using the available JVM tools.
Essentially, this creates an AI-powered chatbot that can understand natural language requests about JVM monitoring (like "show me all Java processes" or "give me a thread dump of process 1234") and automatically use the appropriate tools from JvmTools to fulfill those requests. It's a conversational wrapper around the JVM diagnostic capabilities.
Step 6: REST Endpoint
The REST endpoint finally glues all of it together and let’s us talk with the llm.
package org.acme;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/inspect")
public class InspectorResource {
@Inject
JvmInspector ai;
@GET
@Produces(MediaType.TEXT_PLAIN)
public String inspect(@QueryParam("query") String query) {
return ai.chat(query);
}
}
Step 7: Run and Inspect
Start Quarkus in dev mode:
quarkus dev
Then try:
curl "http://localhost:8080/inspect?query=list+running+java+processes"
Potential output:
I found the following JVMs:
- PID: 12345, displayName: java -jar myapp.jar
- PID: 56789, displayName: io.quarkus.runner.GeneratedMain
Then:
curl "http://localhost:8080/inspect?query=thread+dump+process+56789"
And the LLM will invoke the threadDump
tool, showing the full state of all threads, including the ones we intentionally deadlocked.
### **1. Deadlock Between Threads**
**Thread 1 (`deadlock-1`)**:
- **Blocked on**: `java.lang.Object@639e8881` (owned by `deadlock-2`)
- **Locked**: `java.lang.Object@6ad7c7f`
**Thread 2 (`deadlock-2`)**:
- **Blocked on**: `java.lang.Object@6ad7c7f` (owned by `deadlock-1`)
- **Locked**: `java.lang.Object@639e8881`
Why This Would be a Great MCP Use Case
This example shows Tool calling for the LLM. But it is a generic use-case for all JVMs and an ideal candidate to be implemented via the Model Context Protocol (MCP). With MCP, tools like getJvmProcesses
and threadDump
can be self-described, composable, and deterministically invoked by the model. And more importantly, they can be re-used. Learn more about how to create Quarkus MCP server.
What You’ve Built
You now have a fully working AI-powered JVM inspector built in Quarkus. It uses Dev Services to run an LLM, exposes real diagnostics as tools, and wraps the whole thing in a simple REST interface.
From here, you can:
Extend it to use
jstack
viaProcessBuilder
to inspect other JVMs.Add memory metrics, GC logs, or flight recorder dumps.
Deploy it into a Kubernetes-native observability dashboard.
Create an MCP Server from it.
You’re no longer just logging observability metrics — you’re conversing with your infrastructure.