Blog

Product news, updates, and insights from ej-technologies.

Read with RSS. Subscribe by email. Follow on .

How I Doubled jclasslib's Performance with JProfiler MCP

2026-04-24
Posted by Ingo Kegel

Recently, I improved the performance of jclasslib, my open-source JVM bytecode viewer, by using Codex and a profiling agent. Not because I had a particular pain point, but because I was trying out our MCP server that shipped with JProfiler 16.1. Read on for how the agent more than doubled class file reading, and why profiling no longer needs a reason to start.

The Starting Point

jclasslib consists of two main parts: a library that reads and writes bytecode, and a Swing UI that displays and edits it. The project was started before ASM, the industry standard for reading class files, existed. Since the viewer is tied to the representation produced by the class file reader, it never made sense to switch.

ASM is heavily optimized for resource usage and implements a visitor pattern instead of reading the full structure into memory, as jclasslib does. That was never a concern, because reading the handful of class files that are actually displayed takes very little time. However, the new Find usages feature in jclasslib 7.0 scans the entire classpath. Class file reading speed was suddenly noticeable, but still OK, so I did not pursue it.

Blog figureBlog figure

The Optimization

When JProfiler MCP became available, I asked Codex to take a stab at making class file reading faster, and in no time it produced a series of commits. The clearest illustration of what the agent found is this one. The commit removes an expensive double map creation. This was something I would never have noticed in the code, and even when looking at profiling data it fell into the bucket of "that's just what the program needs to do". Codex, however, traced the map creations back to their two origins and folded them into one.

That one commit took the profiled unit test from 662 ms to 410 ms, a 38% speedup. Other commits in the same run filled in the rest:

  • cb992aac: duplicate map insertion removed from the read path
  • c7c0155d: CharArray allocation skipped for pure-ASCII UTF-8 entries
  • 4798108d: lazy decoding of constant-pool strings
  • a1ae6e7d: classpath scanning sped up for Find usages

Plus a handful of smaller ones. Cumulatively, reading every class file in the JDK 25 JRE (25000+ classes) went from 851 ms to 347 ms, making class file reading almost two and a half times faster. That is where the "doubled" in the title comes from.

Source Inspection vs. Profiling Data

I was curious whether the coding agent could have found this without the JProfiler MCP server, so I asked. I have left the answer below unedited, because the shape of the agent's reasoning matters as much as the conclusion it reaches.

Q: How important was the JProfiler MCP, and how much better was the result compared to just inspecting the source code?

A: It was important for this task. Without the profiler, I would likely have spent time optimizing the wrong thing first.

  • It gave a fast path from "run this exact test under profiling" to usable hotspot data. prepare_profiling returning a ready-to-pass JVM arg worked well.
  • The workflow was simple and composable: prepare_profiling → run Gradle test → check_statusget_performance_hotspotsexpand_performance_hotspot.
  • The hotspot expansion was good enough to distinguish parser cost from harness cost.
  • It directly surfaced the real win: eager reverse constant-pool indexing in ClassFile.setConstantPool, plus its downstream String.hashCode / HashMap.put cost. That was not the most obvious source-level guess.
  • It prevented a likely misoptimization. Pure source inspection made ConstantUtf8Info.readUTF look like the main target. Profiling showed that was hot, but not the best first fix.

How much better than source inspection?

  • Qualitatively: substantially better.
  • Source inspection alone would probably have led to first trying to optimize modified UTF-8 decoding, maybe looking at JRT walking overhead, or maybe trying generic micro-optimizations in structure reading.
  • Profiling showed the highest-value fix was elsewhere: removing eager reverse-index construction on every class read.
  • That produced a measured improvement from about 662 ms to 410 ms, roughly a 38% speedup.
  • I do not think I would have found that specific fix as the first move from source inspection alone.

Overall importance

  • For this task: high.
  • It changed the order of operations from "guess, patch, benchmark" to "measure, patch the biggest real cost, verify".
  • The final result was both faster and more defensible because it was tied to observed hotspots, not intuition.

Overtaking ASM

Something I did not expect happened along the way: jclasslib overtook ASM in the modes that matter most for a viewer. The AsmPerformanceComparisonTest in the repo scans every class in the JRE with both libraries and reports the ratio of jclasslib time to ASM time. A ratio of 1.0 means parity, below 1.0 means jclasslib is faster. At the pre-optimization baseline on JDK 17, jclasslib was about twice as slow as ASM in read-only mode (ratio 1.95). At HEAD, the same benchmark on the same 26,653 classes now reports:

  • Read full class structure: 0.77 (jclasslib 347 ms, ASM 453 ms)
  • Read and rewrite class: 0.87 (jclasslib 917 ms, ASM 1050 ms)
  • Read, decode bytecode, rewrite: 1.23 (jclasslib 1311 ms, ASM 1068 ms)

That is rather remarkable, because the architecture of jclasslib requires a class file structure that is built up in memory, while in the ASM architecture the class file just "streams by". ASM still pulls ahead when the bytecode itself has to be decoded into an instruction list and then re-emitted, which is where its streaming model saves an intermediate representation. For everything else, the gap is gone.

Profiling Without a Reason

Profiling used to need a reason. It was something you did when an app got slow or ran out of memory. With a coding agent and an MCP server, that reason is no longer required. The coding agent can fix performance problems in the background, compare your app's performance against a baseline, and act if necessary.

The JProfiler MCP server is crucial, though. Without it, coding agents will confidently perform "optimizations" that are not helpful and often break your code. The profiling data gives the signal that something needs work and also provides the means to verify that the work was successful.

Give it a try on the JProfiler MCP page. If your app has a hotspot you cannot see by reading the code, the agent will find it.

Archive