VerifyError: Fix Java Bytecode Verification
Fix VerifyError fast: find the class built against a mismatched dependency, rebuild all modules with one JDK, and never mask it with -Xverify..
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java build basics
- ✓Classpath concepts
- ✓A JDK plus javap and jar tools
- VerifyError means the JVM's bytecode verifier rejected a class it can't prove type-safe — the class never runs, so the service fails at load time
- The top trigger is a class compiled against a different dependency version than the one shipped, so diff the graphs and rebuild everything with one JDK
- Stale bytecode agents and shading tools inject frames the modern verifier rejects, so test-boot without -javaagent entries first
- Never ship -Xverify:none: it hides the proof without fixing the cause and trades a loud startup failure for silent corruption
Think of airport security for luggage. Your bag goes through the scanner before it is allowed on the plane. The scanner does not care who packed it — if the shape inside breaks the safety rules, the bag does not fly. VerifyError is the scanner rejecting the bag. Something changed the contents after packing: a tool repacked it badly, an old lock fails the new rules, or two bags got zipped together. You do not argue with the scanner; you repack the bag correctly.
The deploy went out on a quiet Tuesday. No code changes, just a JDK bump from 11 to 17 and an updated base image. Then every pod crashed on boot with the same wall of text: VerifyError, a class name you recognize, and a message about stack maps that reads like the JVM speaking another language. Rollback fixed it instantly, which proved the code was fine and the environment wasn't.
VerifyError is the JVM refusing to run bytecode it can't prove safe. The verifier checks every method's type behavior before the class is allowed to execute — stack heights, value types at every branch, legal object construction. When a class was compiled against a different dependency version, woven by an outdated agent, or shaded together from two releases, those proofs break and the class is rejected at load time.
You'll learn what the verifier actually checks, why JDK upgrades shake out stale frames, how agents and shading corrupt bytecode, how to read the notoriously dense error message, and why -Xverify:none is a trap rather than a fix. By the end, a VerifyError will read like directions instead of noise.
What Bytecode Verification Actually Checks
Before any class executes, the JVM's verifier proofs it the way an auditor checks books: every instruction must leave the operand stack at a legal height, every value must have a provable type at every branch target, and no object may be used before its constructor finishes. The checks run at link time, which is why VerifyError kills startup instead of one request — the class is rejected before a single instruction runs. This is the foundation of Java's type safety: bytecode from any compiler or tool must pass the same audit.
For modern class files the verifier doesn't guess types by inference. It reads the StackMapTable attribute — explicit frames the compiler emitted describing the stack and locals at each branch target — and validates the code against them. That design is fast and strict: a missing frame, a frame that disagrees with the code, or a jump to a target with no frame all fail immediately. Messages like expecting a stackmap frame at a branch target sound cryptic, but they're precise — they name the exact offset where the proof broke.
This strictness is what makes the error useful. The verifier never rejects healthy bytecode, so a failure always means the class file or its inputs are wrong: version skew, a stale rewriting tool, merged duplicates, or a damaged file. Your job isn't to argue with the audit — it's to find which input corrupted the books. Read the class name and offset from the message, then work outward to the dependency, agent, or packaging step that produced those bytes.
Compiled Against One Version, Shipped With Another
The most frequent corrupt input is version skew: a class compiled against one shape of a dependency but shipped with another. If a superclass gained or lost members, an interface hierarchy shifted, or a method's descriptor changed between compile and runtime, the caller's bytecode can reference types and members that no longer line up. The verifier follows those references while proving type safety, and when the hierarchy contradicts the compiled assumptions, the proof collapses and the class fails verification instead of running with a lie.
Multi-module builds invite this quietly. Module A compiles against version 1 of a shared library while the packaged service ships version 2, because nearest-wins mediation resolved them differently or a snapshot refreshed halfway through the build. Each module is internally consistent, so every unit test passes; only the assembled artifact is broken, and only the verifier — which sees the final combination — notices. This is why module-green plus artifact-red is the signature of skew rather than a code bug.
The cure is boring and total: one dependency set, one JDK, one clean build. Diff the compile graph against the packaged graph with dependency:tree, align the versions in dependencyManagement, then rebuild every module from clean so all bytecode shares the same assumptions. Verify afterward with javap -v that sibling classes carry matching major versions. Skew can't survive that pipeline — there's exactly one version of everything for the verifier to check.
Stack Map Frames After a JDK Upgrade
JDK upgrades shake out frame problems that older runtimes tolerated. Since class file version 50 the verifier requires StackMapTable frames and type-checks against them; it no longer infers types for newer classes the way early JVMs did. Bytecode produced by retired compilers, old weaving plugins, or outdated shading tools may carry missing or malformed frames that previous verifiers accepted leniently and current ones reject. The code didn't change — the audit got stricter, and the upgrade applied the new rules to old bytes.
The version table is worth memorizing: Java 8 writes major 52, Java 11 writes 55, Java 17 writes 61. When a VerifyError follows a JDK bump, run javap -v on the named class and check its major version and frame attributes against healthy siblings. A class at an older major beside freshly compiled ones, or frames that reference types that moved between releases, points straight at a module or tool that didn't make the upgrade trip with everything else.
Fix it by bringing the stragglers forward: recompile every module with the pinned JDK and upgrade bytecode tools — AspectJ, Lombok-adjacent weaving, coverage, shading — to releases that support it. Pin the JDK in your build image so no developer machine or refreshed CI image silently mixes toolchains. JDK upgrades are safe when the whole toolchain moves as one unit; they're Russian roulette when half the pipeline still emits yesterday's frames.
Agents, Shading, and Rewritten Bytecode
Agents and bytecode tools rewrite methods after compilation, and every rewrite must preserve verifiable frames. Coverage instruments, APM probes, and load-time weavers all inject instructions into your methods; if the tool's bytecode library predates your JDK, the injected code carries obsolete or missing frames and the whole class fails verification. The stack trace then names your class for a crime committed by the tool — deeply confusing until you've seen it once.
Shading does the same damage statically. Merging two releases of one library into a fat JAR can place a stale copy of a class ahead of the fresh one, or splice constant pools in ways that break frame references. The artifact contains two truths and the verifier checks whichever loads first. Split packages across JAR inputs make this worse by scattering one package's classes across files with different provenance.
Isolate the rewriter in seconds: boot once with all -javaagent entries removed. If the service starts, re-add them one at a time until it breaks — that's your culprit, and its upgrade is the fix. For shading, list duplicates with jar tf piped through sort and uniq -d, and align inputs so each class has exactly one source. Treat every bytecode tool as a versioned dependency with a JDK support matrix, because that's precisely what it is.
Reading the Message Instead of Fearing It
VerifyError messages are dense but formulaic once you know the grammar. They name the fully qualified class, the method, the bytecode offset where the proof broke, and the failed expectation — wrong stack height, unexpected type, missing frame at a branch target. The offset plus javap -c output lets you find the exact instruction; the expectation tells you which proof failed. Expecting a stackmap frame means a branch lands where no frame exists. Bad type on operand stack means a hierarchy changed under compiled assumptions.
Work the message outward in three steps. First, extract the class and check its major version and frames with javap -v — stale major or absent StackMapTable means old toolchain output. Second, ask how the class was produced: compiled here, woven by an agent, or merged by shading — jar tf and the launch flags answer that. Third, diff compile versus runtime dependencies for anything in that class's hierarchy. One of those three steps names the culprit in nearly every case we've seen.
What the message never means is that your source logic is wrong. Verification failures are about the bytes, not the algorithm — the same source recompiled cleanly with an aligned toolchain passes. So resist the urge to rewrite the method. Read the offset, check the provenance, align the inputs, rebuild. Teams that rewrite code against a verifier message waste days tuning logic the verifier never questioned.
-Xverify:none — Why the Easy Flag Is a Trap
Every VerifyError thread eventually attracts the suggestion: just add -Xverify:none. It works, in the narrowest sense — verification is skipped, the class loads, the service boots. And it is always the wrong fix. The verifier exists to prove bytecode can't forge object references, overflow the stack, or use uninitialized objects. Disabling it doesn't repair the broken frames or the skewed hierarchy; it runs them anyway, converting a loud, precise, pre-execution failure into silent memory corruption or a crash in unrelated code hours later.
The flag's only legitimate role is diagnosis in a throwaway environment: if the service boots with verification off, you've confirmed the failure is verification-related rather than missing classes or resources. That confirmation takes minutes and the flag comes right back off. There is no staging use, no temporary production use, no weekend use — ARTIFACTS that can't pass verification are definitionally unsafe to run, and the longer they run the further the corruption spreads from the original lie.
Make the wrong fix impossible structurally. Forbid the flag in launch scripts and container entrypoints via code review checks, alert on its presence in process arguments, and document the real fixes beside it: align dependencies, upgrade the agent, dedupe the shading, rebuild with one JDK. The teams that survive VerifyError fastest are the ones that treat the verifier as an ally delivering a precise bug report — because that's exactly what it is.
- Xverify:none disables the check that proves bytecode can't forge references or corrupt memory. The broken class then runs unchecked, and the precise startup failure becomes silent data corruption or a stranger crash later. Use it only in a scratch environment to confirm a diagnosis — never in production, staging, or any artifact that ships.
The APM Agent That Couldn't Speak JDK 17
- Agents are dependencies with compatibility matrices, not invisible plumbing. Every JDK upgrade must include an agent compatibility check or the verifier will do it for you at 3 AM.
- Staging only protects you when it mirrors production exactly. A newer agent on staging turned the safety net into decoration.
- Boot the real artifact with the real agents in CI. A thirty-second boot test would have caught this before a single pod rolled.
| File | Command / Code | Purpose |
|---|---|---|
| com | public class Branches { | What Bytecode Verification Actually Checks |
| com | public class ParserClient { | Compiled Against One Version, Shipped With Another |
| com | public class LegacyIo { | Stack Map Frames After a JDK Upgrade |
| com | public class RuntimeCheck { | Agents, Shading, and Rewritten Bytecode |
| com | public class ClassFileVersion { | -Xverify:none |
Key takeaways
Common mistakes to avoid
5 patternsAdding -Xverify:none to silence the error
Rebuilding only the failing class with a newer JDK
Upgrading the JDK while keeping an old -javaagent
Shading two versions of one library into a fat JAR
Assuming the class file on disk is intact
Interview Questions on This Topic
What is VerifyError and when does the JVM throw it?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Exceptions. Mark it forged?
6 min read · try the examples if you haven't