On This Page
The error, all of it
Error: Unable to initialize main class Lab
Caused by: java.lang.VerifyError: Expecting a stackmap frame at branch target 9
Exception Details:
Location:
Lab.pick(I)I @1: ifle
Reason:
Expected stackmap frame at this location.
Bytecode:
0000000: 1a9e 0008 043c a700 0505 3c1b ac
OpenJDK 21.0.12.1, Linux x86-64, java -cp v1 Lab, default flags. Nothing is wrong with the source. It's nine lines and you'll see all of them in a minute. Something rewrote the bytes after javac was done with them, and the JVM noticed before running a single instruction.
That's a java VerifyError. If you've landed here from a stack trace that says Bad type on operand stack, Bad return type, Inconsistent stackmap frames or Bad local variable type, you're in the right place; they're the same family. The words after "VerifyError:" are the part that matters, and the Reason: block under it is more useful than the headline. Most people paste the headline into a search box and get a thread from 2012 telling them to add -noverify. Don't. I'll get to why.
I spent the better part of two days on this one, mostly because I assumed I knew what the verifier did and I only half did. The lab below is the result, and it's written up so you don't have to repeat it.
The short version: the JVM re-checks bytecode, and your bytes weren't checked by anyone
When a class gets loaded, the JVM doesn't trust it. It runs a verifier over every method, without executing anything, to prove the code can't do the things that would corrupt the JVM itself: treat an int as an object reference, say, or jump into the middle of an instruction. This happens during linking, after loading and before initialization, so a failing class never runs a line.
javac output passes verification (barring a compiler bug, and I haven't met one). A VerifyError therefore almost always means one of two things. Either the bytes you're running are not the bytes javac produced, because something rewrote the class: an agent, a shading tool, an obfuscator, a weaver. Or they're javac's bytes running against a different version of some other class than the one they were compiled against.
One wrong instinct I had at the start: that this is a "corrupt jar" problem, like a truncated download. It isn't. A corrupt file fails earlier, at parse time, with ClassFormatError or ClassNotFoundException. A VerifyError means the file parsed fine and the code in it is wrong.
The verifier is part of HotSpot, in the class loading and linking path. It's not the compiler and it's not the JIT. If it's firing, no heap or collector setting will change the outcome.
Break a class on purpose, five ways
What you need. OpenJDK 21.0.12.1 (the Ubuntu 24.04 build; any 21 will do), python3 for the byte patching, and jar. No Maven, no ASM. I patch class files by hand so the only moving part is the JVM.
java -version
# openjdk version "21.0.12.1" 2026-08-18
The code. Lab.java. Two branches on purpose, because branches are where the verifier needs help:
public class Lab {
static int pick(int x) {
int r;
if (x > 0) r = 1; else r = 2; // two branches => two StackMapTable frames
return r;
}
public static void main(String[] a) {
System.out.println(pick(5)); // no string concat: see the dead end below
}
}
mutate.py reads a .class, patches it, writes it:
import sys, struct
src, dst, mode = sys.argv[1:4]
b = bytearray(open(src, 'rb').read())
if mode.startswith('nostack'):
i = b.find(b'StackMapTable'); assert i > 0
b[i:i+13] = b'StackMapTablX' # same length, so every offset stays valid
if mode == 'nostack49': b[6:8] = struct.pack('>H', 49) # major version lives at bytes 6-7
if mode == 'nostack50': b[6:8] = struct.pack('>H', 50)
if mode == 'ireturn2areturn': # ireturn (0xac) after iload_1 -> areturn (0xb0)
j = b.find(bytes([0x1b, 0xac])); assert j > 0; b[j+1] = 0xb0
if mode == 'iload2aload': # iload_0 (0x1a) before ifle -> aload_0 (0x2a)
j = b.find(bytes([0x1a, 0x9e])); assert j > 0; b[j] = 0x2a
open(dst, 'wb').write(b)
Renaming the attribute is a neat trick: the JVM ignores attributes it doesn't recognise, so the method looks like it has no stack map, with no other byte shifted.
Steps.
Step 1: compile and run the control
$ javac --release 21 Lab.java && java -Xlog:class+init=info -cp . Lab
Expected: prints 1; log has "Start class verification for: Lab" and "End class verification for: Lab"
Step 2: strip the StackMapTable (major version stays 65)
$ python3 mutate.py Lab.class w_nostack/Lab.class nostack
$ java -Xlog:class+init=info -cp w_nostack Lab
Expected: "Verification for Lab failed" and the VerifyError at the top of this post
Step 3: same strip, but pretend the class is older
$ python3 mutate.py Lab.class w_nostack50/Lab.class nostack50 # major 50
$ python3 mutate.py Lab.class w_nostack49/Lab.class nostack49 # major 49
$ java -Xlog:class+init=info -cp w_nostack50 Lab
Expected (50): "Fail over class verification to old verifier for: Lab", then prints 1
Expected (49): no failover line, no error, prints 1
Step 4: corrupt the code itself, leave the frames alone
# iload_0 (0x1a) becomes aload_0 (0x2a); and ireturn (0xac) becomes areturn (0xb0)
$ java -cp v5 Lab
java.lang.VerifyError: Bad local variable type
Reason: Type integer (current frame, locals[0]) is not assignable to reference type
$ java -cp v4 Lab
java.lang.VerifyError: Bad type on operand stack
Reason: Type integer (current frame, stack[0]) is not assignable to reference type
Step 5: library skew, no byte patching at all
# v1src/B.java: public abstract class B { public abstract String id(); } (same file in v2src)
# v1src/A.java: public class A extends B { public String id() { return "A"; } }
# v2src/A.java: public class A { public String id() { return "A"; } } // 2.0 dropped "extends B"
# appsrc/User.java: static B make() { return new A(); } + main printing make().id()
$ javac -d v1 v1src/*.java && javac -d v2 v2src/*.java && javac -d app -cp v1 appsrc/User.java
$ java -cp app:v1 User # prints A
$ java -cp app:v2 User
Expected: VerifyError: Bad return type
Type 'A' (current frame, stack[0]) is not assignable to 'B' (from method signature)
Forcing levers. Everything here is tiny so you see the error in under a second. Production has the same failure on a 40 MB shaded jar; the size changes nothing about the mechanism.
The confirmation signal. Verification for Lab failed in the class+init log, immediately followed by the exception. If you see a ClassFormatError instead, you broke the file structure, not the code (see the dead end below).
Environment differences. The verifier messages come from HotSpot, not the vendor, so I'd expect other 21 builds to match. I ran everything on 21.0.12.1 only. I haven't run JDK 17 or 25.
Cleanup. Nothing big is written. If the -Xverify:none runs later in this post crashed your JVM, delete the hs_err_pid*.log they leave in the working directory.
The dead end
My first Lab.java printed "pick(5)=" + pick(5). Steps 2 and 3 worked. Then I set the major version to 50 and 49 and the JVM said:
java.lang.ClassFormatError: Class file version does not support constant tag 18 in class file Lab
Constant tag 18 is CONSTANT_InvokeDynamic, which javac emits for string concatenation since JDK 9, and which doesn't exist in a version-49 class. I'd taken a class that used a Java 9 feature and relabelled it as Java 5. The JVM rejected it before the verifier ever ran, which is exactly the ClassFormatError versus VerifyError split I'd said was easy. Removing the concat fixed the lab. It cost me a while, and it was the best accidental demonstration in the whole exercise.
How a verifier proves a method is safe without running it
Look at the bytecode for pick:
0: iload_0
1: ifle 9
4: iconst_1
5: istore_1
6: goto 11
9: iconst_2
10: istore_1
11: iload_1
12: ireturn
StackMapTable: number_of_entries = 2
frame_type = 9 /* same */
frame_type = 252 /* append */
offset_delta = 1
locals = [ int ]
The verifier walks this method once, top to bottom. It keeps a model of the current frame: what type is in each local variable slot, and what type sits at each position on the operand stack. iload_0 pushes an int. ifle pops one and branches. Straight-line code is easy, because each instruction has one predecessor and the model flows through.
Branches are the trouble. Offset 11 can be reached from two places: falling through from 10, and jumping from 6. The verifier needs a single answer for "what's in the frame at 11". The old verifier, the one class files below version 50 still go through, works it out itself by iterating a dataflow analysis until the types stop changing.
The newer design (type-checking verification, which arrived with class file version 50, Java 6) hands the answer over in advance. javac writes the expected frame at every branch target into the StackMapTable attribute, and the verifier only has to check each claim: at offset 9, is the real incoming state assignable to the stated frame? No fixpoint to chase, which is the point of the design. The two frames above are for offset 9 (same as the initial frame) and offset 11 (append an int local).
So the first error from my lab reads exactly like it says. Offset 1 is ifle jumping to 9. The verifier looked for a frame at 9, found none, and gave up. "Expected stackmap frame at this location."
Everything else in the VerifyError family is the same check failing at a different place: Bad type on operand stack is an instruction popping the wrong type; Bad local variable type is a load from a slot holding the wrong type; Bad return type is the thing on the stack not assignable to the method's declared return. "Assignable" is the interesting word. To decide whether A is assignable to B, the verifier has to know A's superclass chain. So it loads A while verifying User:
[0.044s][info][class,load] B source: file:/tmp/vlab/skew/v2/
[0.044s][info][class,load] A source: file:/tmp/vlab/skew/v2/
Both lines print before User.main runs. Verification loads classes as a side effect, which matters later when you're wondering why a library that "isn't used yet" got opened.
Now the surprise from Step 5, which I didn't predict. I ran the same skew experiment first with B as an interface. It did not produce a VerifyError:
java.lang.IncompatibleClassChangeError: Class A does not implement the requested interface B
at User.main(User.java:3)
The verifier is lenient about interface assignability, and the check shows up later, at the first call. If your skew bug involves an interface changing, you get a different exception, at a different time, and nothing for a verifier flag to touch. I changed B to an abstract class to get the error I wanted.
Inside HotSpot: two verifiers, one failover, and who gets skipped
I read verifier.cpp at tag jdk-21+35 through a fetch summariser, not line by line in an editor, so treat the function names below as pointers and not citations. What it says, and what my lab confirmed:
Verifier::verifypicks a path by the class file's major version. Version 50 and up goes to the type-checking verifier (ClassVerifier::verify_class). Below 50 goes straight toinference_verify, which calls into the old C verifier in a native library namedverify(if that library can't be linked you get "Could not link verifier").- There's a failover. If the new verifier throws for a class below major version 51 (
NOFAILOVER_MAJOR_VERSION), HotSpot logs "Fail over class verification to old verifier" and tries again with the inference verifier. That's why Step 3 behaves differently at 50 and 65: major 50 got a second chance, major 65 didn't. I saw that line in theclass+initlog and nowhere else. - Whether a class is verified at all depends on its loader.
Verifier::should_verify_forreturns theBytecodeVerificationLocalflag for the boot loader andBytecodeVerificationRemotefor everything else.
That last one has a measurable effect. Defaults on 21.0.12.1, from -XX:+PrintFlagsFinal:
bool BytecodeVerificationLocal = false {diagnostic} {default}
bool BytecodeVerificationRemote = true {diagnostic} {default}
Both are diagnostic flags, so you need -XX:+UnlockDiagnosticVMOptions to touch them. In the control run, exactly one class is verified: Lab. Turn on -XX:+BytecodeVerificationLocal and I counted 409 Start class verification lines. The JDK's own classes are trusted and skipped. Only code arriving from the app, extension or custom loaders pays for verification and gets checked.
The version table, as far as I can verify it:
Major 49 and below no StackMapTable; old inference verifier only
Major 50 (Java 6) new verifier first, falls back to old one on failure
Major 51+ (Java 7+) new verifier only; a missing or wrong stack map is fatal
JDK 13 -Xverify:none and -noverify deprecated (warning text on 21 says so)
JDK 21 both Bytecode Verification flags are diagnostic; Local off, Remote on
JDK 24 verifying retransformed JDK classes started failing (see below)
I couldn't fetch JVMS section 4.10 this run (the page chunk stopped well before it), so the version-50 and 51 behaviour above rests on the HotSpot source summary and my lab runs, not on reading the spec text. I also did not find any JEP that removes -Xverify:none; the JEP index I could reach listed none for JDK 22 to 26, and I couldn't see anything open for 27.
Where this bites: agents, shaded jars, and a flag that makes it worse
Agents and weavers. This is the most common source I know of, and the one with the most hand-waving in forum answers. A java.lang.instrument transformer gets the class bytes, changes them, hands them back. If it adds a branch and doesn't regenerate the frames, you get my Step 2 error. I reproduced it with a small agent that does what a sloppy one does: it changes the class bytes and leaves the stack map describing the old code. (Exaggerated, but the verifier can't tell the difference.)
import java.lang.instrument.*;
import java.security.ProtectionDomain;
public class Strip {
public static void premain(String args, Instrumentation inst) {
inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader l, String name, Class<?> c,
ProtectionDomain pd, byte[] bytes) {
if (!"Lab".equals(name)) return null;
byte[] out = bytes.clone();
byte[] from = "StackMapTable".getBytes();
outer:
for (int i = 0; i < out.length - from.length; i++) {
for (int j = 0; j < from.length; j++) if (out[i + j] != from[j]) continue outer;
out[i + from.length - 1] = 'X'; // attribute name no longer recognised
return out;
}
return null;
}
});
}
}
$ printf 'Premain-Class: Strip\n' > m.txt && javac Strip.java && jar cfm strip.jar m.txt Strip*.class
$ java -javaagent:strip.jar -cp w Lab
Caused by: java.lang.VerifyError: Expecting a stackmap frame at branch target 9
Same error, same offsets, and it's the agent's output that failed, not the class on disk. If the class on disk runs fine without the agent and fails with it, you've found the culprit. The fix is then on the agent's side, so check which agent version you were on before the upgrade that broke you.
There's a sharp edge from March 2025 on hotspot-dev ("Verification in agent transformers"). An Elasticsearch engineer saw a VerifyError on JDK 24 only, and only through retransformClasses, never on the first load-time transform. As I read the thread, Coleen Phillimore said JDK classes aren't normally verified because they're trusted, that verification on retransform was a recent JDK 24 change, and that classes passing through the class-file-load hook weren't being verified at all, with "we should fix that". Alan Bateman noted that boot-loader classes aren't verified when modified at load time but are when redefined at runtime. No bug number is in the messages I read. The practical bit: retransform failures come back from JVMTI with only JVMTI_ERROR_FAILS_VERIFICATION, no message, and -Xlog:verification is how you get the text. I tried that flag on 21 and it works:
[info][verification] Verifying class Lab with new format
[info][verification] Verifying method Lab.pick(I)I
[info][verification] Verification for Lab failed
17 lines for the broken class, 7 for the good one. Cheap enough to leave on while you debug.
Library skew. Step 5. Your app was compiled against library 1.x; the container ships 2.x. It fails at the first use of the class, not at startup, and often only on one code path, so it looks intermittent. The Reason: line names both types: Type 'A' ... is not assignable to 'B'. Open both jars and diff the hierarchy.
The flag everyone copies. -Xverify:none, and its twin -noverify. On 21.0.12.1 it prints:
OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.
It still works. In my runs it behaved like -XX:-BytecodeVerificationRemote: verification off for everything outside the boot loader. I ran my broken classes with it:
- The stripped-stack-map class ran and printed 1. The code was actually fine; only the frames were missing.
- The
iload_0toaload_0class did this:
# Internal Error (macroAssembler_x86.cpp:830), pid=1348, tid=1349
# fatal error: DEBUG MESSAGE: illegal bytecode sequence - method not verified
with exit code 134 and an hs_err_pid file. The JVM itself aborted.
- The skew case, with the flag on, threw
AbstractMethodError: Receiver class A does not define or inherit an implementation of the resolved method 'abstract java.lang.String id()'. The same bug, one call later, with a worse message.
So -Xverify:none hides the one check that tells you why you're broken, and when the bytecode really is wrong it replaces a clean exception with a crash or a late one. People reach for it to save startup time too. I didn't measure that, so I won't tell you what it saves; I'd want the number before I'd trade a crash for it.
Look-alikes. ClassFormatError is the parser giving up (Step 3's tag 18). NoSuchMethodError and IncompatibleClassChangeError come from resolution, later, and are the usual result of skew when interfaces are involved. VerifyError is the one where the file was fine and the logic was not.
Fixing it without turning verification off
Order I'd try, with what each costs:
- Find who touched the bytes. Run without the agent, the shading plugin, or the weaver. If the error disappears, you've got your culprit. Cost: ten minutes.
- Get the real reason.
-Xlog:verification(info level is enough) names the method and the failing check. TheReason:block in the exception already has the types and the bytecode offset; read it before anything else. - Upgrade the tool that rewrites bytecode. Agents and weavers have to produce correct frames for whatever class file version you feed them, and when the JDK moves, an older build of the tool is the first suspect. I'd try a newer agent release before touching anything on the
javacommand line. Cost: a vendor upgrade and a regression run. - Fix the classpath skew. Align the library version at build time and at runtime, and diff the class hierarchy of the type named in
Reason:. For shaded jars, check that a relocated package didn't leave a reference to the old name.
What's not a fix: -Xverify:none (above); -XX:-BytecodeVerificationRemote (the same thing, with an unlock flag in front); and, on classes with major version under 51, relying on the old verifier's failover to paper over broken frames. That one works, it's why legacy jars survive, and it's exactly why a stack-map bug in a weaver can stay invisible on Java 6 targets and then show up when someone moves the target to 8.
Which JDK versions behave how, and what to test on an upgrade
Things I'd check when moving LTS to LTS (17 to 21, 21 to 25):
- Run the app once with
-Xlog:verification=infoand-Xlog:class+init=infoand grep forfailed. A verification failure that was hidden behind failover or a skipped loader on the old JDK will show up here. - Re-run with your agents attached. Agent-generated bytecode is the case most likely to change with a JDK version, because the verifier and the class file version both move. The JDK 24 retransform discussion is the example I have, and I haven't reproduced it.
- Look for
-Xverify:noneand-noverifyin every startup script, container entrypoint andJAVA_TOOL_OPTIONS. They're deprecated since 13; I found no removal, but I wouldn't count on that staying true. - On JDK 21 these flags work with a warning. On later versions I haven't run them, so assume nothing.
JDK 17 and 25 were not run for this post. Everything above with a version number is either from my 21.0.12.1 lab or from a source I name.
Keep these
- A VerifyError almost always means the bytes aren't the ones
javacwrote, or they'rejavac's bytes against a different library than they were compiled against. - The
Reason:block under the exception is the diagnosis. The headline isn't. - On class files version 51 and up, a missing or wrong
StackMapTableis fatal. Version 50 gets a failover to the old verifier. Below 50 there's no stack map at all. -Xverify:noneran my broken classes. One of them crashed the JVM. It's deprecated since JDK 13 and it doesn't fix anything.- Interface changes surface as
IncompatibleClassChangeErrorat the first call, not as a VerifyError. Different exception, same root cause.
Questions people ask about java VerifyError
What causes java.lang.VerifyError?
The JVM's bytecode verifier decided a method's code isn't type-safe: wrong type on the operand stack, a missing stack map frame at a branch target, or a return type that doesn't match. In practice it's a bytecode-rewriting agent or tool, or a library whose class hierarchy changed after your code was compiled.
How do I fix "Expecting a stackmap frame at branch target"?
Something added or moved a branch without regenerating the StackMapTable. Run without your agent or weaver to confirm, then upgrade it. Check with -Xlog:verification=info to see which method failed. I reproduced this on JDK 21.0.12.1.
Is -Xverify:none safe to use?
No. It's deprecated since JDK 13 (the warning says so on 21) and it only skips the check. In my lab it let a broken class crash the JVM with exit code 134 and let a skewed library fail later with AbstractMethodError.
Why does my class pass on Java 8 but fail on Java 17?
Possible if the class file is below major version 51, where HotSpot falls back to the old verifier when the new one rejects it. Newer targets don't get that second chance. See how class loading, linking and initialization work for where verification sits in that sequence.