Beyond a missing class, what do the JVM errors VerifyError, ClassFormatError, UnsupportedClassVersionError and UnsatisfiedLinkError each signal, and at which point of loading a class does each one arise?
answer
- Parse -> ClassFormatError
- Version too new -> UnsupportedClassVersionError (a ClassFormatError)
- Verify -> VerifyError, usually generated bytecode
- Native -> UnsatisfiedLinkError: path, arch, or symbol
- All are LinkageError subtypes
basics
~20 sAll are LinkageError subtypes. ClassFormatError: the bytes are malformed during parsing. UnsupportedClassVersionError: a well-formed file whose class-file version is newer than the JVM supports. VerifyError: bytecode fails the verifier during linking. UnsatisfiedLinkError: a native method or native library cannot be linked.
solid answer
~50 sThey map onto successive points of making a type usable. **`ClassFormatError`** comes from *parsing*: wrong magic number, truncated file, illegal constant-pool entry. Usually a corrupt artifact, a file mangled by a text-mode transfer, or a bad bytecode generator. **`UnsupportedClassVersionError`** is a specific `ClassFormatError` subtype: the file parses but its major version is above what this JVM accepts, for example class-file version 65 (Java 21) on a Java 17 runtime. The message prints both numbers. It is a toolchain mismatch, not corruption. **`VerifyError`** comes from *verification*, the first linking step: the bytecode is well-formed but not type-safe, the operand stack does not balance, or the stack map frames do not match. In practice it means generated or instrumented bytecode is wrong, an agent rewrote a class badly, or someone hand-edited a class file. **`UnsatisfiedLinkError`** comes from *native* linking: `System.loadLibrary` cannot find the library on `java.library.path`, the library is the wrong architecture, or a `native` method has no matching implementation symbol when first invoked.
code
text · 3 linesjava.lang.UnsupportedClassVersionError: com/example/App has been compiled by a more recent
version of the Java Runtime (class file version 65.0), this version of the Java Runtime only
recognizes class file versions up to 61.0go deeper
Recall the one-line meaning of each: malformed file, too-new class file version, unverifiable bytecode, missing native library or symbol.
Place each at its stage of loading, know that UnsupportedClassVersionError is a ClassFormatError subtype, and read the version numbers in the message correctly.
Move straight to the environment: align build and runtime JDKs with --release, suspect agents and bytecode rewriters for VerifyError, and check architecture, java.library.path and symbol names for native failures.
Prevent them structurally: one pinned toolchain across build and runtime images, controlled use of instrumentation agents with frame recomputation, and native artifacts published per architecture with a startup check that fails the deploy rather than the first request.
## Where each error sits in the pipeline Making a type usable proceeds as: read the bytes -> **parse and define** -> **link** (verify, prepare, resolve) -> **initialize**. Each of these errors belongs to a specific point, and knowing which point tells you where to look. All four extend `LinkageError`, which extends `Error`. They are not conditions application code is expected to recover from; they mean the artifact or the environment is wrong. ## ClassFormatError: the bytes are not a class file Raised while parsing the byte array into an internal class representation. Triggers include: the file does not start with the magic number `0xCAFEBABE`; it is truncated; the constant pool contains an index out of range or a tag the version does not allow; attribute lengths do not add up. Real causes are almost always mechanical, not logical: an artifact corrupted in transit or by a text-mode copy, a build that wrote a partial file, a resource filter that ran a template substitution over `.class` files, a JAR assembled from mismatched fragments, or a byte-code generation library emitting something structurally invalid. Note that a class file inside a JAR can also be reported this way if the archive itself is damaged. ## UnsupportedClassVersionError: right format, wrong era This is a subclass of `ClassFormatError`, and the distinction matters because the diagnosis is entirely different. Every class file carries a major/minor version. Roughly: 52 = Java 8, 55 = Java 11, 61 = Java 17, 65 = Java 21. A JVM refuses any file whose major version exceeds the one it implements; it never runs "a bit newer" bytecode. The message names both sides, for example *class file version 65.0, this version of the Java Runtime only recognizes class file versions up to 61.0*. The cause is a toolchain split: compiled by a newer JDK than the one running, a dependency published for a newer baseline, a Docker image whose JRE is older than the build image, or an IDE compiling with a different release setting than the build. The fix is to align the runtime with the build, or to compile with `--release` targeting the lowest runtime you must support. Nothing about the classpath contents is wrong; only the target level is. ## VerifyError: well-formed but not type-safe Verification is the first linking step and exists to guarantee that bytecode cannot violate the JVM's safety invariants regardless of who produced it. The verifier checks, among other things, that the operand stack never underflows or overflows, that types flowing into each instruction match what it expects, that local variable slots are read with the type they were written with, that control transfers land on real instruction boundaries, and that `final` methods are not overridden. Since class-file version 50/51 the verifier is primarily the fast **StackMapTable**-driven one: the compiler records the expected types at each branch target, and the verifier checks assignments against those frames instead of running an expensive dataflow inference. A consequence is that *incorrect or missing stack map frames* are themselves a verification failure. That is why `VerifyError` in the wild overwhelmingly means machine-generated bytecode is wrong: a bytecode-manipulation library used at the wrong API level, an instrumentation agent rewriting a method and forgetting to recompute frames, an obfuscator, or a class built by hand. Ordinary javac output essentially never fails verification. A rare but instructive variant appears when frames were computed against a *different* view of the class hierarchy than the one present at run time, because frame checking involves assignability between types, which requires loading those types. ## UnsatisfiedLinkError: the native side is missing This one is about the boundary to native code, and it has two distinct shapes. First, **library loading**: `System.loadLibrary("foo")` or `System.load("/abs/path/libfoo.so")` fails. Causes: the library is not on `java.library.path`; the file exists but is built for a different architecture or OS (an x86-64 `.so` on an arm64 machine, which is a frequent container/Apple-silicon problem); a *transitive* native dependency of that library is missing so the OS loader refuses it; or the same library is already loaded by a different class loader, which is forbidden, one native library may be loaded by at most one loader in the JVM. Second, **symbol resolution**: the library loaded, but when a `native` method is first invoked the JVM cannot find the corresponding entry point. Causes: the exported symbol does not match the expected mangled name (package, class and method, with an overload suffix when the method is overloaded), the implementation registered methods in `JNI_OnLoad` and missed one, or the header was regenerated after a rename. ## Reading them quickly A short decision path: *Does the message name a class-file version?* Toolchain mismatch. *Does it say the file is malformed or the magic is wrong?* Corrupt artifact or bad generator. *Does it name a method and complain about types, stack or frames?* Bytecode rewriting. *Does it name a library or a native method?* Native path, architecture or symbol naming. All four are environment/packaging defects, and none is fixed by changing application logic.
- Why is VerifyError so rare from plain javac output but common when using instrumentation agents?javac emits bytecode together with correct StackMapTable frames, so the verifier's checks pass by construction. Agents and bytecode-generation libraries rewrite method bodies, and if they change the operand stack or local types without recomputing frames, the recorded frames no longer describe the code and verification fails. That is why such libraries expose an option to recompute frames, at some build-time cost.
- Can you run a class compiled by a newer JDK on an older JVM by lowering the source level flag alone?No. What matters is the class-file version written into the artifact, which is set by the target/release level, not by the source level. Compiling with --release pinned to the oldest supported runtime is the reliable fix, because it also restricts the API surface to that release, avoiding NoSuchMethodError against newer methods.
saying these in an interview costs you the question
- Treating UnsupportedClassVersionError as a classpath or dependency-missing problem
- Assuming a JVM can run bytecode one version newer than itself
- Blaming application logic for VerifyError instead of the tool that generated or rewrote the bytecode
- Thinking UnsatisfiedLinkError always means the file is absent, missing wrong-architecture and missing-symbol cases
- Claiming verification happens at initialization rather than during linking