How Java actually runs.
Yesterday we said Java compiles to bytecode and the JVM runs it. Today we open that black box — just enough that no Spring 'magic' ever feels like real magic again.
Why this chapter exists
Most Java courses skip this entirely or drown you in 50 pages of memory areas. Both are wrong. The truth is: Spring, Hibernate, JPA, and every annotation-driven framework rely on a handful of JVM features. If you don't see those features once, the frameworks always feel like sorcery.
The features we need to understand are exactly four:
- The compilation pipeline — how
.javabecomes running instructions - The classloader — how Java finds and loads your classes (Spring uses this aggressively)
- The runtime memory model — heap, stack, metaspace (you'll hear these in interviews)
- Reflection — how Java code can inspect and call other Java code at runtime (this is how every annotation works)
1 · The compilation pipeline
Yesterday we said: Java compiles to bytecode. The JVM runs the bytecode. Let's get sharper than that.
Stage 1 · Source → Bytecode (compile time)
You run javac HelloWorld.java. The compiler produces HelloWorld.class — a binary file containing bytecode. Bytecode is like assembly for an imaginary CPU. It's portable: same on every OS.
Stage 2 · Bytecode → JVM (runtime)
You run java HelloWorld. The JVM starts, loads the class, and begins executing the bytecode. Initially it interprets bytecode — reads each instruction and emulates it. That is slow.
Stage 3 · JIT compilation (the secret sauce)
The JVM watches which methods run frequently ("hot" methods). For those, the JIT (Just-In-Time) compiler generates real native machine code on the fly and replaces the interpreted version. From then on, those methods run at near-C speed.
Pure interpretation = portable but slow. Pure ahead-of-time compilation = fast but not portable. JIT gets both: portable bytecode that becomes fast machine code only for code paths that actually run a lot. This is why a long-running Java server eventually performs as well as C++ for hot endpoints.
2 · The Classloader — how Java finds your code
Here's a question almost no Java tutorial answers: when you write new UserService(), how does the JVM find the UserService class? It's not in memory yet. It lives in some .class file somewhere in your classpath or JAR.
The answer: the ClassLoader. ClassLoaders are special Java classes whose only job is finding bytecode and bringing it into memory.
When the JVM needs a class:
- It asks the App ClassLoader: "do you have
UserService?" - The App CL first asks its parent (Platform CL) — delegation
- Platform CL asks its parent (Bootstrap)
- If Bootstrap doesn't have it, the question bubbles back down until someone finds it
- If nobody finds it:
ClassNotFoundException💥
When you see NoClassDefFoundError, ClassNotFoundException, or "two versions of the same class on classpath" errors — that is the classloader. Spring Boot's "fat JAR" relies on a custom classloader that knows how to read classes from inside a nested JAR. This is the "magic" of java -jar app.jar.
3 · Memory model — heap, stack, metaspace
When the JVM runs, it splits its memory into regions. You don't need every detail. You need three:
Heap
Every object you create with new lives on the heap. Shared by all threads. The Garbage Collector sweeps unreferenced objects from here. When you hear "OutOfMemoryError: Java heap space" — this is the area.
Stack (one per thread)
Each thread has its own stack. Each method call creates a "frame" with that method's local variables. When the method returns, the frame is gone. Object references live on the stack, but the actual objects they point to live on the heap. This distinction matters in interviews.
Metaspace
When the ClassLoader loads User.class, it stores the class definition (methods, fields, bytecode) here — not on the heap. Before Java 8 this was called "PermGen." You only need to remember: classes themselves live in Metaspace.
Code: User u = new User();
- The variable
usits on the stack (it's a local variable). - The object created by
new User()sits on the heap. - The class definition
User.classsits in Metaspace.
Garbage collection — the one-paragraph version
The GC runs in the background, finds objects on the heap that no thread can still reach (no live reference points to them), and frees their memory. You never call free() in Java. This is huge — but the cost is occasional pauses called "GC pauses." For 99% of apps this is invisible. For ultra-low-latency systems (high-frequency trading), engineers pick GC algorithms carefully (G1, ZGC, Shenandoah). You don't need this depth now. Just know: memory is automatic, but not free.
4 · Reflection — Java looking at itself
Here is the JVM feature that powers every Java framework you'll touch.
Reflection is the ability of Java code, at runtime, to:
- Take a class as input (
User.classorClass.forName("com.app.User")) - Read all its fields, methods, annotations
- Create instances of it without calling
newdirectly - Call its methods by name
// Without reflection
User u = new User();
u.setName("Abhishek");
// With reflection — same effect
Class<?> cls = Class.forName("com.app.User");
Object u = cls.getDeclaredConstructor().newInstance();
Method m = cls.getMethod("setName", String.class);
m.invoke(u, "Abhishek");The reflection version looks horrible. Why would anyone use it? Because frameworks need it.
When you write @Autowired UserRepository repo; — Spring is using reflection. It scans your classes, sees the annotation, finds a matching bean, and uses reflection to set the field for you. No magic. Just reflection + a registry of beans.
Same with @Entity, @Column, @Transactional, @RestController. Every annotation in Java is just metadata that some framework reads via reflection at runtime.
Once you internalize this, Spring stops being magical. It's just a well-organized program that reads your annotations and wires things together using reflection — exactly what you could write yourself if you had the patience.
One more piece — proxies (preview)
Reflection lets a framework read your code. But Spring sometimes needs to wrap your code — for example, to start a database transaction before your method runs and commit it after. How?
Spring uses dynamic proxies: at runtime, it generates a brand-new class that wraps yours, intercepting method calls. We won't go deep here — Week 3 (AOP) is dedicated to this. But know that proxies + reflection together are how:
@Transactionalwraps methods in transactions@Cacheableintercepts method calls and returns cached results- Spring Data JPA generates implementations of your repository interfaces with no code
The full picture
Lock in today's learning
Answer in your own words. If you can't, that's the section you re-read.
- What does the JIT compiler do, and why does it make Java fast?
- What does a ClassLoader actually do? Why does Spring Boot need a special one?
- Where do objects live? Where do local variables live? Where do class definitions live?
- What is reflection in one sentence?
- How does
@Autowireduse reflection? Describe in 2–3 lines. - What's the difference between
NoClassDefFoundErrorat startup vs. at runtime? (hint: classloader scope)
End of Day 2. You now know more about how Java runs than 80% of mid-level Java developers.