Something in your JVM is rewriting class files while your application runs. It might be your mocking library stubbing a final method, your APM agent weaving timers into service calls, or your ORM generating a proxy for lazy loading. Roughly 40% of the libraries in a typical Maven dependency tree touch bytecode at some point, and nearly all of them pick one of three tools to do it: Byte Buddy, Javassist, or ASM.
Pick the wrong one and you get stack traces nobody on your team can read, VerifyError at startup, or an agent that breaks the moment a user upgrades their JDK. Pick the right one and instrumentation becomes a boring, maintainable part of your codebase.
TL;DR: The Quick Verdict
Use Byte Buddy if you want a fluent Java API, first-class agent support, and JDK-version portability without hand-writing bytecode — it is the default choice for new instrumentation work. Use Javassist if you need to inject source-level code into existing classes quickly (the source-level API compiles Java fragments at runtime) and you value a small API surface over raw speed. Use ASM only when you need maximum control over generated bytecode, want zero runtime dependencies, or you are building the low-level machinery that other tools are built on. Byte Buddy itself sits on top of ASM.
Side-by-Side Comparison (data pulled 2026-09-11)
| Byte Buddy | Javassist | ASM | |
|---|---|---|---|
| GitHub stars | 6,891 | 4,232 | not on GitHub (OW2-hosted) |
| Last repo push | 2026-09-04 | 2026-09-02 | 2026-05-23 (Maven Central) |
| Latest artifact | net.bytebuddy:byte-buddy 1.18.13 | org.javassist:javassist 3.33.0-GA | org.ow2.asm:asm 9.10.1 |
| License | Apache-2.0 | MPL-1.1 / LGPL-2.1+ / Apache-2.0 (triple) | BSD-3-Clause |
| API level | High (fluent, type-safe) | Medium (source fragments or bytecode) | Very low (raw opcodes) |
| Runtime dependency | ASM (shaded) | none | none |
| Best for | Agents, proxies, frameworks | Quick patching, teaching, prototyping | Compilers, agents, tooling |
| Learning curve | Gentle | Moderate | Steep |
Which One for Which Job
| Use case | Recommended | Why |
|---|---|---|
| Java agent that instruments methods at class load | Byte Buddy | AgentBuilder + ClassFileLocator handle retransformation, JDK class files, and bootstrapping for you |
| Throwaway class patching in a test harness | Javassist | ClassPool plus a Java source string is faster to write than any visitor pattern |
| Building a mocking framework | Byte Buddy | subclass()/rebasing() semantics are explicit, and generated types are debuggable |
| Writing a new language compiler backend | ASM | Direct opcode control, method-size and frame computation built in |
| Shipping a dependency-free library | ASM | One jar, zero transitive dependencies |
| Legacy codebase stuck on JDK 8 tooling | Javassist | Battle-tested since 1999, stable API for a decade |
Hooking into a running JVM via premain | Byte Buddy | Built-in premain support with AgentBuilder.Default() |
Byte Buddy: The Modern Default
Byte Buddy (6,891 stars, Apache-2.0, last pushed 2026-09-04) replaced raw ASM calls in most frameworks that needed readable instrumentation code. Its pitch is that you describe what the resulting class should look like instead of emitting instructions.
The canonical example from the project README generates a subclass of Object whose toString() returns a fixed string:
| |
Delegation to existing code is where Byte Buddy earns its keep. You write a plain interceptor, annotate the parameters, and Byte Buddy wires the generated method to it:
| |
Maven coordinates:
| |
For agents, AgentBuilder handles the hard parts — matching loaded and future classes, retransforming existing ones, and dealing with classes already loaded by the bootstrap loader. The annotation-driven variant lets you keep the matching logic in an annotated class instead of a builder chain.
Where it hurts: Byte Buddy shades its own copy of ASM, which adds roughly 4 MB to your fat jar. Generated class names are intentionally unstable unless you set .name(...) — fine for tests, confusing when a stack trace reads Example$ByteBuddy$8lK2pQ. Startup instrumentation of thousands of classes also costs real milliseconds; measure before weaving everything.
Javassist: Source-Level Patching Without a Compiler
Javassist (4,232 stars, last pushed 2026-09-02) predates most modern tooling and is still maintained by Shigeru Chiba. Its distinguishing feature is a two-level API: you can edit bytecode directly, or you can hand it Java source text that it compiles on the fly against the target class’s own type information.
The classic load-time patch from the official tutorial:
| |
The $1, $2 placeholders address method arguments, $0 is this, and $_ is the return value — a compact vocabulary that makes surgical instrumentation readable. That same feature is why Javassist is still a first choice for teaching instrumentation and for prototypes where the goal is to prove an idea in ten minutes.
Javassist is triple-licensed (MPL 1.1, LGPL 2.1 or later, Apache 2.0), which matters if you are embedding it in a commercially licensed product — you can take the Apache option.
Where it hurts: The runtime compiler is slower than direct bytecode emission, and the generated fragments must be valid Java with explicit casts in places. Editing a class after it has been loaded requires retransformClasses on modern JDKs, and the tutorial is explicit that insertBefore on a method whose class was already loaded behaves differently. Errors surface as CannotCompileException with a line number inside the string you injected, so keep injected fragments to one or two statements.
ASM: The Foundation Everything Else Uses
ASM is not on GitHub in an official capacity. Its home is OW2 (asm.ow2.io), and its artifacts ship through Maven Central — the current release is org.ow2.asm:asm:9.10.1 with metadata last updated 2026-05-23. It is BSD-3-Clause licensed, dependency-free, and small.
ASM gives you a visitor pipeline. You read a class, walk it with a ClassVisitor, optionally wrap the MethodVisitor it hands you for each method, and write the result back out:
| |
| |
In agent code, you register a ClassFileTransformer that returns reader-to-writer output for matching classes, then call instrumentation.addTransformer(transformer, true) so already-loaded classes can be redefined.
Where it hurts: VerifyError: Bad type on operand stack is the signature failure mode, usually caused by writing instructions without matching the frame computation strategy — COMPUTE_FRAMES fixes most of it at the cost of loading classes during generation. Exception tables, local variable slots, and line numbers must all be managed by hand. When an ASM bug ships, the blame lands on you, not on a framework. That is the real price of maximum control.
How the Three Fit Together
The hierarchy is worth internalizing: ASM is the engine, Byte Buddy is the ergonomic layer over ASM, and Javassist is an independent implementation with a source-level front end. If you are choosing for a new agent or framework, Byte Buddy is the pragmatic answer and it is what most contemporary projects picked. If you are choosing for a compiler or a tool that must run inside another JVM already containing ASM, go direct to ASM and relocate your copy (maven-shade-plugin with a relocation rule) to avoid classpath conflicts.
For related reading, see our JVM build tools comparison to understand how generation and packaging interact, our Java JSON libraries guide if you are assembling a service stack, and our Java embedded HTTP servers comparison for the runtime side of the same decision.
Pitfalls: Where Bytecode Engineering Bites Back
- Classloader leaks are the number one production incident. Every generated class holds a reference to the loader that defined it. Injecting helper types from your agent’s loader into an application class creates a permanent link that keeps the application loader alive after redeploy, and in a servlet container that shows up as
OutOfMemoryError: Metaspaceafter a few redeploys. - Do not instrument JDK core classes casually. Everything you weave into
java.langis inherited by every thread, and a bug there is a JVM-wide outage. Add packages to a skip list before going live. - Signature changes break binary compatibility. Adding a parameter to a method you are instrumenting silently invalidates the interception;
@RuntimeType(Byte Buddy) or adapter methods (Javassist) exist to absorb that churn. - Frames are not optional since Java 7. Any hand-written bytecode that jumps requires valid stack map frames. Turning on
COMPUTE_FRAMESis easier than debugging a verify error at 2 AM. - Instrumentation costs latency, not just CPU. A weave on every method call in a hot loop can add double-digit percentages. Measure with a load test, and prefer
@Advice.OnMethodEnter-style boundaries over per-instruction hooks. - Native images do not support runtime generation. GraalVM needs build-time reachable metadata for generated classes; if you plan to ship one, verify your baking approach early rather than after migrating.
FAQ
Is Byte Buddy just a wrapper around ASM? Functionally yes for the generation path — Byte Buddy uses ASM to emit class files, and shades its own copy. It adds a typed builder API, agent support, and class-file locating logic on top. That layer is what you are paying the jar-size cost for.
Is Javassist still maintained in 2026? Yes. The repository was last pushed on 2026-09-02 and the 3.33.0-GA release appeared on Maven Central in August 2026. Development pace is slower than Byte Buddy’s, but the API has been stable for years, which is exactly what legacy integration code wants.
Why is ASM not on GitHub?
ASM is hosted by OW2, the middleware consortium that has maintained it since its origin at France Télécom. There is no official GitHub mirror; the llbit/ow2-asm repository is a stale third-party mirror. Use Maven Central artifacts and the official documentation at asm.ow2.io.
Which one should I use for a Java agent?
Byte Buddy. AgentBuilder handles retransformation, boot-classpath isolation, and matching of classes loaded before or after installation. Doing the equivalent by hand with ASM means writing your own transformer registry and error handling.
Can I avoid bytecode manipulation entirely?
Sometimes. If you need a proxy, java.lang.reflect.Proxy covers interface-based cases with no dependencies. For method timing, a hand-written wrapper or a build-time annotation processor avoids runtime generation altogether. Reach for a bytecode toolkit when the class is third-party and you cannot recompile it.
How much does the toolkit choice affect runtime performance? Generated code performance is nearly identical across all three; what differs by an order of magnitude is generation time and startup. ASM emits fastest, Javassist’s source compiler is slowest, and Byte Buddy sits in between while offering the most conveniences.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com