JVM Bytecode
Purpose
Read what was actually compiled, rather than what the source appears to say. The failures
this skill prevents are the ones that only bytecode can settle: an intermittent VerifyError
from instrumentation whose final instructions and stack maps disagree, a coverage
agent that silently instruments nothing after a JDK upgrade, a build that stops on
code too large, and a performance argument built on the shape of source code when the
shape that runs is different — string concatenation that is now invokedynamic, a switch
that is a SwitchBootstraps type switch, a one-line try-with-resources that is 38 bytes of
bytecode, boxing that nobody wrote.
Bytecode is a typed stack machine plus symbolic constant-pool references. Most instructions carry indices rather than embedded names. The JVMS permits lazy or eager resolution while constraining when failures become observable; dynamically computed constants/call sites have their own lazy bootstrap rules. Loading, verification, resolution and initialization are distinct—inspect the failure phase rather than assuming “first execution” universally.
Workflow
- Preserve and inspect the actual artifact first. Record its digest, selected JAR entry,
loader/module, compiler release/options and target vendor/build. Recompile only for a separate
reproduction; use
-g -parameterswhen useful without overwriting the incident artifact. Examples here use JDK 25; Class-File API needs JDK 24+, not an implicit project upgrade.javapfor signatures,javap -cfor the code,javap -c -pto include private members,javap -vfor the constant pool, the attributes and theStackMapTable(-vdoes not imply-p). - Read the header before the instructions.
major_version, the access flags andthis_classanswer most version questions on their own — seereferences/javap-and-class-file-anatomy.md. - Classify a
LinkageErrorby its message before forming a hypothesis.VerifyError,ClassFormatError,UnsupportedClassVersionErrorand a lateNoSuchMethodErroridentify different failure categories; locate the actual producer separately. JDK 25 texts are inreferences/limits-and-failure-catalogue.md. - Establish locals and stack state before tracing opcodes. Derive parameter slots from
the descriptor/access flags;
LocalVariableTableis optional debug metadata with scoped names and reused slots. Slot 0 isthisin an instance method; category-2long/doublevalues occupy two local slots. - Classify every call site by instruction and symbolic reference. Static/special calls
have different resolution/selection rules from receiver-dispatched virtual/interface
calls; javac 11+ may encode private nestmate calls with virtual/interface opcodes.
invokedynamiclinks a call site through a bootstrap. Bytecode form is not the final dispatch cost after JIT compilation. Seereferences/dispatch-and-abstraction-cost.md. - Separate what bytecode can and cannot answer. It shows interpreter-level instruction
semantics, symbolic allocations/calls, and the code size consumed by JIT policy. It does
not show what the JIT inlined, which
speculation held, or how long anything took — hand those to
compilation-and-inlining-logsandjit-compilation. - For runtime-generated bytecode, dump it and disassemble the dump — the generated class
is the one the verifier rejected, not the original source. On JDK 25 the lambda dump is
-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles=true; the older property is silent. - Answer any cost question with a measurement, on the hardware and JDK in question, with the flags declared alongside the number.
Rules
- Prefer
javapor the Class-File API over a decompiler for exact dispatch/attribute questions. Decompilers reconstruct a readable approximation and re-sugar details that matter—implicit boxing and the real dispatch form. - Never assume the same source produces the same bytecode across JDK versions. String
concatenation has been
invokedynamicsince JDK 9 (JEP 280), private calls have beeninvokevirtualsince JDK 11 (JEP 181), patternswitchusesSwitchBootstrapssince JDK 21. These are javac lowering choices for the selected target, not guarantees about every compiler or class file. Explicit builders, constant-folded concatenation and--release 8output differ. Inspect control flow around concat call sites rather than using a keyword alone. - Treat javac primarily as a lowering compiler, with limited folding/simplification whose
exact output can change. A measured one-line try-with-resources example was 38 bytecode
bytes—above one tested cold-site inline threshold but still eligible under other hot/policy
paths. The 8,000-byte huge-method cutoff applies only with the corresponding HotSpot policy
and has version/tier exceptions. The 64 KB class-file limit that stops the build is a
different number from the 8,000-byte HotSpot policy cutoff—see the desugaring table in
references/dispatch-and-abstraction-cost.md. - For modern feature releases,
major_version = JDK version + 44; JDK 25 is 69. An individual newer-version class file is rejected by an older runtime. Check both numbers, multi-release JAR selection, build image and runtime image—a--release 25build deployed onto a21-jreimage surfaces as "the application will not start". - A transformer must emit code, exception ranges, maximums and stack maps consistent with the
final body. Recompute frames (ASM
COMPUTE_FRAMES, or Class-File API generation) unless the transformation framework correctly remaps/preserves them. An absent physicalStackMapTableon version 50+ means an implicit zero-entry table; branches that require frames still fail. Frame computation can load classes, so use a loader-aware hierarchy resolver and test circularity/module boundaries. - A bytecode library's version is coupled to class-file versions and is often shaded
inside something else.
Unsupported class file major version 69fromorg.objectweb.asm.ClassReadermeans the agent, coverage tool or mocking library bundles an ASM that cannot parse the rejected class's version; identify its source and upgrade the tool or produce a compatible target artifact as appropriate. AClassFileTransformerthat throws is treated like returningnull: later transformers and class definition still proceed. The class may load uninstrumented and the process may succeed, so gate instrumentation/coverage assertions explicitly rather than trusting exit code. - Test instrumentation against the same JDK major version that runs in production, not only the development one. Include multi-release JAR entries, supported loaders/modules, retransformation and generated classes in the compatibility matrix. Hidden classes have instrumentation restrictions; do not promise that a normal transformer can observe or retransform lambda proxy definitions.
- Treat a Java agent or transformer as privileged production code: pin and verify its artifact, minimize its class/method scope, protect dumped bytecode because it may contain secrets, and make mandatory instrumentation fail an explicit readiness/deployment gate.
- Never disable verification to make a
VerifyErrorgo away.-Xverify:noneand-noverifyhave warned since JDK 13 (JDK-8214719) andBytecodeVerificationRemoteis a diagnostic flag on 25; disabling verification removes a structural/type safety gate and can turn rejection into unsafe execution or a later failure.-XX:-UseSplitVerifieris not ignored — it isUnrecognized VM optionand the JVM does not start (removed in JDK 8, JDK-8009595). invokedynamicis not inherently slow. Resolution invokes a bootstrap and installs a linked call site; concurrent resolution and bootstrap failure rules are subtler than “runs exactly once”. Steady-state cost depends on the resulting target's mutability and JIT visibility.- Lambda proxy classes have been hidden classes since JDK 15 (JEP 371), defined through
MethodHandles.Lookup::defineHiddenClassrather thanUnsafe::defineAnonymousClass. They are not discoverable by name — not viaClass.forName, not viagetDeclaredClasses— and the$$Lambda/0x…suffix is a runtime identity, not a stable sequential index. - Non-capturing lambdas tend to be a single reused instance and capturing ones tend to
allocate per invocation. That is
LambdaMetafactorybehaviour, not a JVMS guarantee. Confirm it with JMH-prof gc; never assert it from the source shape. - A
synchronizedblock usesmonitorenter/monitorexitwith exceptional cleanup; the exact number/ranges of exception-table entries are javac/version/control-flow dependent. A synchronized method usesACC_SYNCHRONIZEDwithout explicit monitor instructions. Both have monitor semantics when locking the same object, but a method locksthis(instance) or its declaringClass(static), while a block locks its evaluated expression and can cover a smaller region. - The tested exhaustive pattern
switchover a sealed type carries a syntheticdefaultthat throwsMatchException. It can signal binary evolution at that default, but a record-pattern accessor throwing can also be wrapped inMatchException. Inspect location and cause before attributing it to stale artifacts. - Treat receiver diversity as a property of a runtime call site, not the language. If it is materially hot, compare accepting dispatch, isolating a stable hot site or redesigning the abstraction; do not add type switches merely to game one JIT profile.
- Never quote a cycles-per-bytecode-instruction table, however plausible it looks. Post-JIT cost depends on microarchitecture, compilation tier, the inline cache state at that specific call site and memory layout. Produce the number yourself and publish it with the hardware, the JDK and the flags.
- Use JMH for any instruction- or abstraction-level cost, never
System.nanoTime()around a loop, and add-prof gcwhenever the question is allocation rather than time. obj instanceof String sandinstanceoffollowed by a cast compile to the sameinstanceof/checkcastpair on javac 25. Prefer the pattern for its scoping, not for a saved instruction that does not exist.- A class file with
minor_version = 0xFFFFdepends on preview features and loads only on exactly that feature release, with--enable-previewat runtime as well as at compile time. Depending on a preview API is enough to set it; passing--enable-previewto a class that uses nothing preview is not. - Verify any suspicious VM flag with
java -XX:+PrintFlagsFinal -version | grep -i <term>before it goes into a runbook; adevelopflag such asHugeMethodLimitis absent from that list on a product build and refuses to start the JVM.
References
- javap and class file anatomy — the
javapinvocations, an annotated JDK 25 disassembly,LocalVariableTableversusMethodParameters, themajor_versiontable and the preview marker, the constant pool entry kinds includingMethodHandle/MethodType/Dynamic, descriptors versusSignature, what the verifier checks with the exactVerifyErrortexts, the JDK 25 lambda dump property, and the Class-File API next to ASM. Read when disassembling anything, when diagnosing aVerifyError, or when writing or fixing a class transformer. - Dispatch and abstraction cost — the invoke
family, tier-specific dispatch/profile behavior, the table of what javac lowers each construct
to and how large it gets (
synchronized,finally, try-with-resources, the threeswitchshapes, records,assert, boxing), lambdas and hidden classes, the abstraction comparison table, and grep recipes that work on modern class files. Read when the question is what a call site costs, which abstraction to choose, or why a small method was not inlined. - Limits and the failure catalogue — the
symptom-to-cause table with JDK 25 message texts for
VerifyError,ClassFormatError,UnsupportedClassVersionError,code too large, lateNoSuchMethodError, and ASM, Byte Buddy and JaCoCo breakage after a JDK upgrade; the JVMS limits; the verification flags and why not to touch them. Read when aLinkageErroror a javac limit error arrives, or when a hot method never appears inPrintCompilation.