Skip to content

Week 5 - Class Loading and Bytecode

Conceptual Core

Java compiles to bytecode, an idealized stack-machine ISA, which the JVM loads, verifies, links, and (eventually) JIT-compiles to native. A class is not just source-level - it's a runtime resource with an identity: the <defining classloader, FQN> pair. Two classes with the same fully-qualified name loaded by two different classloaders are different classes and not assignment-compatible. Internalize this and most production classloader bugs become obvious.

Class loading is lazy, hierarchical (parent-delegation), and the source of a whole genre of production bugs: ClassNotFoundException, NoClassDefFoundError, LinkageError, and metaspace classloader leaks.

The trap

Assuming "the class is loaded" is one event. It's three phases: load → link → initialize. The init phase runs your static {} lazily, the first time the class is actively used. Reading a static final constant does NOT initialize it; calling a static method does. JLS §12.4 enumerates triggers - read them before debugging your next "static block ran twice" bug.

Mechanical Detail

  • Class-file format (JVMS Ch. 4): magic CAFEBABE, constant pool, access flags, fields, methods, attributes. Read it: javap -v -p YourClass.class.
  • Bytecode is a stack machine - ~200 opcodes, you'll meet ~30 daily. invokedynamic (7+) is how lambdas, switch-on-strings, and pattern matching are implemented under the hood.
  • Linking sub-phases: verify (bytecode well-formed), prepare (static field storage), resolve (symbolic refs → direct). Then initialize (<clinit>). Init failure throws ExceptionInInitializerError and bricks the class for the rest of its loader's life.
  • Loader hierarchy (9+): bootstrap → platform → app → custom. Thread.currentThread().getContextClassLoader() exists because ServiceLoader/JDBC/JNDI need to look down the hierarchy, which parent-delegation forbids; the TCCL is the workaround.
  • Custom classloaders power plugin systems, hot reload, multi-tenant isolation, and bytecode-rewriting frameworks (Hibernate, Mockito).
  • Class-File API (JEP 484, stable in 24+) - java.lang.classfile reads/writes class files declaratively. Replaces ASM for new code.

Lab

Take a 10-line Java method. Compile it. Read the javap -v -p output line by line. Then build a class with the same bytecode using the Class-File API, load it via a custom ClassLoader, invoke via reflection, confirm output matches.

Idiomatic Drill

Find a real classloader leak (search "metaspace leak Tomcat"). The canonical pattern: a ThreadLocal<T> whose T is loaded by a webapp classloader, kept alive by a long-lived worker thread that survives redeploy. Fix: ThreadLocal.remove() in a finally - or ScopedValue (final in 25).

Production Hardening Slice

Add to hardening/:

-Xlog:class+load=info,class+unload=info:file=/var/log/jvm-classload.log
-XX:MaxMetaspaceSize=256m
-XX:+HeapDumpOnOutOfMemoryError
Redeploy a small Spring Boot app three times in the same JVM. Diff the log: classes loaded vs unloaded. The delta is your leak budget. A typical mid-size Spring service loads 15-25k classes.

Comments