<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Prashant Gupta</title>
    <description>The latest articles on Forem by Prashant Gupta (@akanksha_gupta_6039d4d9d6).</description>
    <link>https://forem.com/akanksha_gupta_6039d4d9d6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3880331%2F92c3a5b9-1735-45a7-b3b6-224a2e24a17a.png</url>
      <title>Forem: Prashant Gupta</title>
      <link>https://forem.com/akanksha_gupta_6039d4d9d6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/akanksha_gupta_6039d4d9d6"/>
    <language>en</language>
    <item>
      <title>I Built a JVM Profiler That Points AI at Your Exact Line of Broken Code — Here's How It Works Under the Hood</title>
      <dc:creator>Prashant Gupta</dc:creator>
      <pubDate>Wed, 15 Apr 2026 11:33:54 +0000</pubDate>
      <link>https://forem.com/akanksha_gupta_6039d4d9d6/i-built-a-jvm-profiler-that-points-ai-at-your-exact-line-of-broken-code-heres-how-it-works-under-184p</link>
      <guid>https://forem.com/akanksha_gupta_6039d4d9d6/i-built-a-jvm-profiler-that-points-ai-at-your-exact-line-of-broken-code-heres-how-it-works-under-184p</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I've spent the last year building &lt;strong&gt;JVM CodeLens&lt;/strong&gt; — a desktop app that analyzes heap dumps, GC logs, thread dumps, and JFR recordings, correlates every finding to the exact &lt;code&gt;file:method:line&lt;/code&gt; in your source code, and uses an LLM to explain &lt;em&gt;why&lt;/em&gt; it's broken and &lt;em&gt;how&lt;/em&gt; to fix it. It runs entirely on your machine. Your source code and diagnostic data never leave your laptop.&lt;/p&gt;

&lt;p&gt;This post is &lt;strong&gt;not&lt;/strong&gt; a product pitch. It's a breakdown of the engineering problems you hit when you try to build "AI for JVM diagnostics" properly — problems that &lt;code&gt;curl heap.hprof | llm&lt;/code&gt; will never solve — and how I solved them with Eclipse MAT, JavaParser, GCToolkit, QuestDB, and a carefully-scoped LLM integration.&lt;/p&gt;

&lt;p&gt;If you've ever been on-call for a Java service and thought &lt;em&gt;"jstack is giving me a 50,000-line wall of text, and I have no idea which thread matters"&lt;/em&gt; — this is for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: the "last mile" between diagnostic data and source code
&lt;/h2&gt;

&lt;p&gt;Every senior Java engineer has done this dance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Production is on fire. Heap usage climbs, GC pauses spike, latency explodes.&lt;/li&gt;
&lt;li&gt;You SSH in. &lt;code&gt;jmap -dump:live,format=b,file=heap.hprof &amp;lt;pid&amp;gt;&lt;/code&gt;. Copy it out.&lt;/li&gt;
&lt;li&gt;Open Eclipse MAT. Wait 6 minutes for the dominator tree.&lt;/li&gt;
&lt;li&gt;Scroll. You see &lt;code&gt;java.util.concurrent.ConcurrentHashMap$Node&lt;/code&gt; is eating 3.2 GB.&lt;/li&gt;
&lt;li&gt;Now what?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Eclipse MAT will tell you &lt;strong&gt;what&lt;/strong&gt; is leaking. It will not tell you &lt;strong&gt;where in your code&lt;/strong&gt; it's being allocated. Not in a way that maps to &lt;code&gt;ServiceController.java:84&lt;/code&gt; without you manually walking the object graph.&lt;/p&gt;

&lt;p&gt;Same story with every other tool:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;th&gt;What it doesn't&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jstack&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200 threads, 50k stack frames&lt;/td&gt;
&lt;td&gt;Which thread is the problem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jstat -gcutil&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;GC pause stats&lt;/td&gt;
&lt;td&gt;Which allocation pattern caused them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC logs&lt;/td&gt;
&lt;td&gt;Every pause event&lt;/td&gt;
&lt;td&gt;Whether this is normal for your app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JFR&lt;/td&gt;
&lt;td&gt;Hot methods, allocation samples&lt;/td&gt;
&lt;td&gt;What they mean in &lt;em&gt;your&lt;/em&gt; domain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;jmap histo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Top classes by instance count&lt;/td&gt;
&lt;td&gt;Where those instances come from in code&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;last mile&lt;/strong&gt; — from "your heap is full of &lt;code&gt;String[]&lt;/code&gt;" to "line 84 of &lt;code&gt;CacheLoader.java&lt;/code&gt; is appending to a list that's never cleared" — is where every engineer burns their Friday evening.&lt;/p&gt;

&lt;p&gt;That's the gap JVM CodeLens closes.&lt;/p&gt;




&lt;h2&gt;
  
  
  "Why not just throw the heap dump at Claude?"
&lt;/h2&gt;

&lt;p&gt;This is the first question everyone asks. It's a reasonable question. Let me explain why it doesn't work, because the answer reveals the whole design philosophy of the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 1: Heap dumps are binary blobs, and big
&lt;/h3&gt;

&lt;p&gt;A production heap dump is often &lt;strong&gt;2–8 GB&lt;/strong&gt; of binary HPROF data. Context windows can't hold that. You'd also be shipping your entire object graph — including user PII, session tokens, cached JWTs — to an LLM provider. That's a compliance nightmare.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 2: LLMs hallucinate on unstructured data
&lt;/h3&gt;

&lt;p&gt;If you hand an LLM 200k lines of thread dump text and ask "what's the deadlock?", it will invent one. Confidently. With fake line numbers. I've watched it happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reason 3: The hard parts of JVM diagnostics are deterministic, not AI-shaped
&lt;/h3&gt;

&lt;p&gt;Finding a deadlock in a thread dump is a &lt;strong&gt;graph traversal problem&lt;/strong&gt;. You build a lock-waits-for graph and detect cycles. This is a solved CS problem. You don't need GPT-4 to do it — you need 40 lines of Java. Same for dominator trees, GC pause percentiles, object retention graphs. These are &lt;strong&gt;computations&lt;/strong&gt;, not inferences.&lt;/p&gt;

&lt;h3&gt;
  
  
  The right split
&lt;/h3&gt;

&lt;p&gt;So here's the mental model JVM CodeLens is built around:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Java does the parsing and computation. The LLM does the reasoning.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java&lt;/strong&gt;: parse HPROF with Eclipse MAT → compute dominator tree → extract top retainers → map to source via AST → produce a &lt;strong&gt;structured JSON finding&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM&lt;/strong&gt;: receives the structured finding (a few KB, not GB) → generates the explanation, the hypothesis, the suggested fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The LLM never sees your heap dump. It sees something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"leak_suspect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"retained_mb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3247&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"java.util.concurrent.ConcurrentHashMap$Node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"gc_root_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SessionCache.activeSessions → HashMap$Node[]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allocation_site"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SessionCache.java"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"onLogin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;84&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source_snippet"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"activeSessions.put(userId, new Session(...))"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"retention_trend_24h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+12% per hour, no plateau"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's maybe 2 KB of structured data. It fits in a system prompt. The LLM can reason about it. And critically — &lt;strong&gt;the LLM's answer cites your file and line number because the prompt already contains them&lt;/strong&gt;. It can't hallucinate the location.&lt;/p&gt;




&lt;h2&gt;
  
  
  The source correlation engine (the hard part)
&lt;/h2&gt;

&lt;p&gt;This is the technical core, and it's where most "AI for JVM" attempts hand-wave past the actual problem. How do you get from a runtime class name like &lt;code&gt;com.acme.SessionCache&lt;/code&gt; to the specific file, method, and line in the user's repo?&lt;/p&gt;

&lt;p&gt;Here's the pipeline:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Index the source with JavaParser + JDT
&lt;/h3&gt;

&lt;p&gt;When a user links their Git repo, JVM CodeLens walks every &lt;code&gt;.java&lt;/code&gt; file and builds an AST using &lt;strong&gt;JavaParser&lt;/strong&gt; (for syntax) plus &lt;strong&gt;Eclipse JDT&lt;/strong&gt; (for type resolution — critical for generics and inheritance).&lt;/p&gt;

&lt;p&gt;For each class, we extract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ClassMetadata&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fullyQualifiedName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// com.acme.SessionCache&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;filePath&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// src/main/java/com/acme/SessionCache.java&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;declarationLine&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// 12&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MethodMetadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;FieldMetadata&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AllocationSite&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;allocations&lt;/span&gt;  &lt;span class="c1"&gt;// every `new X()` with its line number&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;AllocationSite&lt;/code&gt; list is the magic. Every &lt;code&gt;new ConcurrentHashMap&amp;lt;&amp;gt;()&lt;/code&gt; in the codebase gets indexed with its exact line number. Later, when we see &lt;code&gt;ConcurrentHashMap$Node&lt;/code&gt; dominating the heap, we can point to every place it could have come from.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Parse the HPROF with Eclipse MAT Core
&lt;/h3&gt;

&lt;p&gt;Eclipse MAT's parser is the industry-standard HPROF engine (it's what Eclipse Memory Analyzer uses). We pull its Core as a library. After parsing, we walk the &lt;strong&gt;dominator tree&lt;/strong&gt; to find retained heap — not just shallow size.&lt;/p&gt;

&lt;p&gt;For the top N retainers, we emit a structured &lt;code&gt;HeapHistogramEntry&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;HeapHistogramEntry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;className&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;instanceCount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;shallowBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;retainedBytes&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Nullable&lt;/span&gt; &lt;span class="nc"&gt;SourceLocation&lt;/span&gt; &lt;span class="n"&gt;sourceLocation&lt;/span&gt;  &lt;span class="c1"&gt;// from the indexer&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Map runtime → source
&lt;/h3&gt;

&lt;p&gt;Given &lt;code&gt;className = "com.acme.SessionCache"&lt;/code&gt;, we look up &lt;code&gt;ClassMetadata&lt;/code&gt; in our index and attach the source location. If the class is ambiguous (multiple matches across modules), we rank by package proximity to the GC root path.&lt;/p&gt;

&lt;p&gt;The result: every row in the heap histogram has a &lt;strong&gt;clickable source link&lt;/strong&gt; that opens the file at the allocation line. Not a guess. Not an AI hallucination. A direct lookup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Same idea, different artifacts
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Thread dumps&lt;/strong&gt; — regex-parse stack frames, resolve &lt;code&gt;com.acme.Foo.bar(Foo.java:127)&lt;/code&gt; against the index, render each frame as a clickable link.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GC logs&lt;/strong&gt; — parse with &lt;a href="https://github.com/microsoft/gctoolkit" rel="noopener noreferrer"&gt;Microsoft GCToolkit&lt;/a&gt; (works across G1, ZGC, Shenandoah, CMS, Parallel), correlate pause spikes with deployment timestamps from &lt;code&gt;git log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JFR&lt;/strong&gt; — use &lt;code&gt;jdk.jfr.consumer&lt;/code&gt; to extract hot methods and allocation events, resolve each &lt;code&gt;StackFrame&lt;/code&gt; against the index.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every finding becomes a structured object with a &lt;code&gt;SourceLocation&lt;/code&gt; attached. Then, and only then, do we hand it to the LLM for reasoning.&lt;/p&gt;




&lt;h2&gt;
  
  
  The storage tier: why QuestDB (embedded)
&lt;/h2&gt;

&lt;p&gt;Early on I tried SQLite for time-series metrics. Bad idea. When you're polling 5 JVMs every 5 seconds and storing 20+ metrics per target, you generate ~7k rows/minute per target. Queries for "show me the last 24 hours of heap usage with 1-minute downsampling" become miserable.&lt;/p&gt;

&lt;p&gt;Switched to &lt;strong&gt;QuestDB embedded&lt;/strong&gt;. Same JAR, no external process. It's a columnar TSDB with SQL, and the SAMPLE BY operator is purpose-built for downsampling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;heap_used_mb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;heap&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;jvm_metrics&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;target_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'app-prod-1'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="nb"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dateadd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'h'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;SAMPLE&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One wrinkle: QuestDB has a native DLL loaded via &lt;code&gt;FunctionFactoryScanner&lt;/code&gt;, which choked on Spring Boot's &lt;code&gt;jar:nested:&lt;/code&gt; packaging. Fix: &lt;code&gt;requiresUnpack("**/questdb-*.jar")&lt;/code&gt; in the bootJar task. Cost me two days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retention and forecasting
&lt;/h3&gt;

&lt;p&gt;Metrics are kept 90 days by default. An hourly summary table (downsampled via &lt;code&gt;SAMPLE BY 1h&lt;/code&gt;) supports forecasting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linear regression&lt;/strong&gt; for monotonic trends (heap growth)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Holt-Winters triple exponential smoothing&lt;/strong&gt; for weekly/daily seasonality&lt;/li&gt;
&lt;li&gt;Combined, they power &lt;strong&gt;Time-to-Failure (TTF) forecasting&lt;/strong&gt;: &lt;em&gt;"At current growth, heap hits &lt;code&gt;-Xmx&lt;/code&gt; in 14 days 3 hours"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are pure Java — no Python, no sidecar. The whole forecasting pipeline is a few hundred lines.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI layer: four providers, one abstraction
&lt;/h2&gt;

&lt;p&gt;LLM integration is designed to be &lt;strong&gt;bring-your-own-key&lt;/strong&gt;. Four providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Anthropic Claude (recommended — best at structured reasoning)&lt;/li&gt;
&lt;li&gt;OpenAI GPT-4&lt;/li&gt;
&lt;li&gt;Google Gemini&lt;/li&gt;
&lt;li&gt;Ollama (local, for air-gapped environments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're behind a single &lt;code&gt;LlmProvider&lt;/code&gt; interface. The prompts differ per artifact (GC/heap/thread/JFR) because the production-troubleshooting playbook for each is different. For example, a GC prompt includes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Severity calibration: ignore pauses &amp;lt; 50ms on G1. Flag p99 &amp;gt; 500ms as degraded, &amp;gt; 2s as critical. Correlate throughput drop with allocation rate spike.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These prompts took months to tune, informed by real production incidents.&lt;/p&gt;

&lt;p&gt;One under-appreciated detail: &lt;strong&gt;streaming&lt;/strong&gt;. JVM CodeLens uses SSE to stream the LLM response into the UI token-by-token. When your production is down, a 15-second "thinking…" spinner feels awful. Streaming makes the tool feel alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  The privacy model
&lt;/h2&gt;

&lt;p&gt;This is non-negotiable and a core product principle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Your heap dumps, thread dumps, GC logs, JFR files, source code — never leave your machine.&lt;/strong&gt; All parsing and correlation happens locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM receives only the structured JSON summary&lt;/strong&gt; (kilobytes), using &lt;em&gt;your&lt;/em&gt; API key on &lt;em&gt;your&lt;/em&gt; account. We don't proxy the request. We don't see it.&lt;/li&gt;
&lt;li&gt;For Ollama users, &lt;strong&gt;nothing leaves your machine at all&lt;/strong&gt;. It's fully offline.&lt;/li&gt;
&lt;li&gt;Only license validation and update checks talk to our servers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is why it's a desktop app (Electron + embedded Spring Boot) instead of SaaS. The privacy model breaks the moment you introduce a server.&lt;/p&gt;




&lt;h2&gt;
  
  
  A real example: diagnosing a leak
&lt;/h2&gt;

&lt;p&gt;Here's what the end-to-end flow looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setup:&lt;/strong&gt; a Spring Boot service, 8 GB heap, steadily climbing over 3 days until OOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1:&lt;/strong&gt; User attaches JVM CodeLens to the running JVM (via local Attach API or JMX). Metrics start flowing into QuestDB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2:&lt;/strong&gt; Anomaly detector notices heap-used-post-GC is trending up with sigma &amp;gt; 3 over the 7-day baseline. It auto-fires a memory leak analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3:&lt;/strong&gt; Triggers a heap dump via the Attach API. Parses it. Top retainer: &lt;code&gt;ConcurrentHashMap$Node&lt;/code&gt; at 6.1 GB retained. GC root path: &lt;code&gt;RequestContextHolder.sessionCache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4:&lt;/strong&gt; Indexer looks up &lt;code&gt;SessionCache.java&lt;/code&gt;. Finds allocation site at line 84:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onLogin&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UserProfile&lt;/span&gt; &lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;activeSessions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// line 84&lt;/span&gt;
    &lt;span class="c1"&gt;// ... no eviction policy. ever.&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 5:&lt;/strong&gt; Builds structured finding, sends to Claude with a scoped prompt. Claude responds:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;SessionCache.activeSessions&lt;/code&gt; is an unbounded &lt;code&gt;ConcurrentHashMap&lt;/code&gt;. Line 84 inserts on every login, but no eviction exists. Over your 3-day window at observed login rate (~2.3k/hour), retained size grew from 400 MB to 6.1 GB — matches the trend. Fix: replace with &lt;code&gt;Caffeine.newBuilder().expireAfterAccess(30, MINUTES).maximumSize(100_000).build()&lt;/code&gt;, or add explicit eviction on logout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Step 6:&lt;/strong&gt; Git correlation service cross-references: line 84 was touched by commit &lt;code&gt;a4f2b19&lt;/code&gt; 4 days ago — author removed the eviction cron job in a "simplification" PR.&lt;/p&gt;

&lt;p&gt;That whole chain — detect → capture → parse → correlate → reason → blame — runs in about 90 seconds on a developer laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Desktop shell:&lt;/strong&gt; Electron 30&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; embedded Spring Boot 3.4 on Java 24 (virtual threads for the metric poller)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React 19 + TypeScript + Vite + D3 + Recharts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPC:&lt;/strong&gt; localhost HTTP + WebSocket (backend is a child process of the Electron main)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-series:&lt;/strong&gt; QuestDB embedded&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config:&lt;/strong&gt; SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heap parsing:&lt;/strong&gt; Eclipse MAT Core&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GC parsing:&lt;/strong&gt; Microsoft GCToolkit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code parsing:&lt;/strong&gt; JavaParser + Eclipse JDT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JFR:&lt;/strong&gt; &lt;code&gt;jdk.jfr.consumer&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JRE:&lt;/strong&gt; Adoptium 21, bundled — no system Java required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole .dmg/.exe/.deb ships with its own JRE. Users don't need Java installed. They don't need to configure anything. Attach the app, pick a JVM, go.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's hard about this that isn't obvious
&lt;/h2&gt;

&lt;p&gt;Three things will surprise you if you try to build something similar:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Packaging a Spring Boot JAR inside Electron is a landmine field.&lt;/strong&gt; Nested classloaders, DLL unpacking, JVM flag passthrough, JRE path resolution across Mac/Windows/Linux. I wrote a &lt;a href="https://jvmcodelens.com/blog/packaging-spring-boot-in-electron" rel="noopener noreferrer"&gt;separate post on this&lt;/a&gt; — it's the kind of thing that takes a week and no one documents it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Correlating a runtime class name to a source file is ambiguous more often than you think.&lt;/strong&gt; Multi-module Gradle builds with the same class name in &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt;. Inner classes. Lombok-generated methods. Kotlin interop. Every one of these breaks the naive lookup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GC logs from ZGC / Shenandoah look nothing like G1.&lt;/strong&gt; You can't regex your way out of it. GCToolkit saves months of work.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;JVM CodeLens is &lt;strong&gt;free for personal use&lt;/strong&gt; (Community tier) — full local JVM monitoring, all parsers, source correlation, and AI with your own API key. Paid tiers add team features, fleet view, cloud license, and enterprise scaffolds (K8s discovery, SSO, IntelliJ plugin).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://jvmcodelens.com" rel="noopener noreferrer"&gt;https://jvmcodelens.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/prguptadev/jvm_code_lens" rel="noopener noreferrer"&gt;https://github.com/prguptadev/jvm_code_lens&lt;/a&gt; (star it if you'd like to follow along)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://jvmcodelens.com/#blog" rel="noopener noreferrer"&gt;https://jvmcodelens.com/#blog&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a Java engineer who's tired of jstack walls-of-text, I'd love your feedback. Especially if you hit a scenario where the source correlation fails — those are the bugs I care about most.&lt;/p&gt;

&lt;p&gt;And if you've built something similar, or have opinions on the &lt;em&gt;right&lt;/em&gt; way to split responsibility between deterministic parsing and LLM reasoning in diagnostic tooling — drop a comment. This design space is still wide open and I'd love to swap notes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? I'm writing a series on building developer tools that live at the intersection of JVM internals and LLMs. Follow me to catch the next one — probably on how the predictive Time-to-Failure engine works without needing ML models.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>java</category>
      <category>performance</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
