HOW THE JVM HANDLES
CLASS LOADING
AND GARBAGE COLLECTION
As Java developers, we often focus on writing features, optimizing APIs, or fixing bugs. All while relying on the JVM to quietly do the heavy lifting underneath. Two of the most crucial mechanisms running behind the scenes are Class Loading and Garbage Collection (GC). These subsystems define how your code enters the runtime and how memory is managed once it’s running.
I’ll break them down in a way that aligns with everyday Java development.
The JVM Class Loading Mechanism
How your .class files come alive
Class loading is the process of taking your compiled bytecode (.class files) and making them available for execution inside the JVM. It’s handled by the ClassLoader subsystem, which works in three main phases:
Phase 1: Loading
The JVM locates the .class file and loads its binary data into memory.
Where do these classes come from?
- check_circle Local file system
- check_circle JARs inside your project
- check_circle Network (yes, possible)
- check_circle Runtime-generated bytecode (e.g., proxies)
Custom ClassLoaders can even load classes from databases, REST APIs, or encrypted sources. This is how frameworks like Spring dynamically generate proxies or enhanced classes.
Phase 2: Linking
This phase prepares the class for execution. It includes three sub-steps:
a. Verification
Checks whether the bytecode is valid and safe.
This prevents malformed or malicious bytecode from crashing the JVM.
b. Preparation
static int counter; // now exists in memory, default = 0
Allocates memory for:
- check_circle Static variables
- check_circle Default values (default int = 0, boolean = false, object references = null)
c. Resolution
Converts symbolic references (e.g., method names, field names) into real memory addresses.
Phase 3: Initialization
Now the static initializers and static blocks run:
static {
System.out.println("Class loaded!");
}This is the moment developers usually notice ClassLoader behavior when logs print unexpectedly early.
The ClassLoader Hierarchy
Here's the thing nobody tells you early in your Java journey: ClassLoaders are not equal. They’re hierarchical.
Just like a family, the JVM asks the parents first before doing anything itself.
- check_circle Bootstrap ClassLoader. The ancestor. Loads core JDK classes.
- check_circle Extension ClassLoader. Loads extension libraries.
- check_circle Application ClassLoader. Loads your application’s classes and libraries.
This hierarchy prevents dangerous overrides. You cannot define your own java.lang.String.
Your ClassLoader will ask the parent, the parent asks its parent, and so on and the JDK classes win every time.
In Java, class loading follows a parent-delegation model.
This means: whenever a ClassLoader wants to load a class, it first asks its parent ClassLoader to try loading it. Only if the parent cannot load it, the child tries.
Because of that rule:
- check_circle The Bootstrap ClassLoader (the top parent) loads all core JDK classes e.g., java.lang.String, java.util.List, java.io.File, etc.
- check_circle Since the Bootstrap ClassLoader loads these classes first, your custom ClassLoader can never override them, because the parent will always answer with the official JDK version.
This prevents someone from writing a malicious version like:
// NOT POSSIBLE
package java.lang;
public class String {
// your own fake implementation
}Even if you put a file like this in your app’s classpath, your ClassLoader will ask the parent first:
- check_circle AppClassLoader → check parent (PlatformClassLoader)
- check_circle PlatformClassLoader → check parent (Bootstrap)
- check_circle Bootstrap → "I already have java.lang.String loaded."
- check_circle Result → Your version is ignored.
This mechanism ensures security and stability, so core Java behavior cannot be modified or hijacked.
JVM Garbage Collection
How Java manages memory so you don’t manually free it.
The JVM heap is where objects live. Over time, unused objects pile up and this is where the Garbage Collector steps in.
JVM Memory Spaces
Young Generation
Objects are created here first.
- check_circle Eden: where new objects are born
- check_circle Survivor S0 & S1: where survivors migrate after GC cycles
Minor GCs happen here frequently and quickly.
Old Generation
Objects promoted from Young Gen live here. GC happens less often (Major GC).
Metaspace
Stores class metadata. No longer limited like the old PermGen.
How the JVM Decides Who Lives and Who Dies
The garbage collector’s rule is simple: “If no one can reach you, you no longer exist.”
GC determines reachability based on: threads, stack frames, static variables, active references.
If nothing points to an object, it becomes garbage.
The Minor GC
When Eden fills up:
- check_circle The JVM pauses threads (briefly)
- check_circle Copies surviving objects to Survivor spaces
- check_circle Clears Eden
- check_circle Promotes long-surviving objects to Old Gen
This is fast because copying memory is fast.
It’s why Java can handle thousands of TPS with proper GC tuning.
Modern Garbage Collectors
G1 GC (default in new Java)
- check_circle Region-based
- check_circle Predictable pause times
- check_circle Used in most modern Spring Boot systems
ZGC
- check_circle Ultra-low latency
- check_circle Even with large heaps (GBs to TBs)
Shenandoah
- check_circle Similar goals to ZGC
- check_circle Concurrent compaction
Your choice of GC can literally determine whether your system survives high traffic.
How Does the JVM Decide When to Run GC?
GC is triggered when:
- check_circle Eden space is full
- check_circle Old Gen occupancy crosses a threshold
- check_circle System memory pressure increases
- check_circle GC algorithms’ internal heuristics decide it's optimal
You can monitor GC activity via:
- check_circle Xlog:gc* (JDK 9+)
- check_circle JDK Mission Control
- check_circle VisualVM
- check_circle Prometheus + Micrometer (Spring Boot)
Final Thoughts Mechanism
JVM Class Loading and Garbage Collection are invisible engines quiet, powerful, and often underappreciated. As Java developers, understanding them gives us superpowers.
It helps us debug smarter, design cleaner systems, and build applications that scale gracefully.
If you're building high-performance services, diving deeper into GC tuning, classloader isolation, and bytecode generation is worth every minute.