JVM & Design Patterns
Understand Java internals and architectural patterns
JVM Architecture and Design Patterns in Java
Master the Java Virtual Machine (JVM) architecture and essential design patterns with free flashcards and spaced repetition practice. This lesson covers JVM internals, memory management, class loading mechanisms, and the most important Gang of Four design patternsβessential concepts for building robust, maintainable Java applications and acing technical interviews.
Welcome to JVM & Design Patterns π»
Welcome to one of the most critical lessons in your Java journey! Understanding how the JVM works under the hood transforms you from someone who merely writes Java code into a developer who can optimize, troubleshoot, and architect sophisticated applications. Combined with design patternsβproven solutions to recurring software design problemsβyou'll gain the knowledge that separates junior developers from senior engineers.
This lesson is divided into two major sections: first, we'll explore the JVM's internal architecture, memory model, and execution engine; second, we'll dive into the most practical design patterns used in real-world Java development. By the end, you'll understand not just what your code does, but how it executes and why certain architectural decisions lead to better software.
Core Concepts: The Java Virtual Machine π§
JVM Architecture Overview
The Java Virtual Machine (JVM) is an abstract computing machine that enables your computer to run Java programs. Its beauty lies in the "write once, run anywhere" philosophyβJava bytecode compiled on one platform runs on any system with a JVM.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β JVM ARCHITECTURE β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β β β βββββββββββββββ ββββββββββββββββ β β β Class Loaderβββββββ Runtime Data β β β β Subsystem β β Areas β β β βββββββββββββββ ββββββββ¬ββββββββ β β β β β β β β β β βββββββββββββββββββββββββββββββββββ β β β Execution Engine β β β β ββββββββββββ ββββββββββββ β β β β βInterpreterβ β JIT β β β β β ββββββββββββ β Compiler β β β β β ββββββββββββ β β β βββββββββββββββββββββββββββββββββββ β β β β β β β β βββββββββββββββββββββββββββββββββββ β β β Native Method Interface β β β βββββββββββββββββββββββββββββββββββ β β β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Class Loader Subsystem π
The Class Loader is responsible for loading .class files into memory. It operates in three phases:
- Loading: Reads bytecode from files and creates a
Classobject - Linking: Verifies bytecode, prepares static fields, and resolves symbolic references
- Initialization: Executes static initializers and static initialization blocks
Class loaders follow a delegation hierarchy:
ββββββββββββββββββββββββ
β Bootstrap ClassLoaderβ (loads core Java classes)
ββββββββββββ¬ββββββββββββ
β
ββββββββββββΌββββββββββββ
β Extension ClassLoaderβ (loads extension classes)
ββββββββββββ¬ββββββββββββ
β
ββββββββββββΌββββββββββββ
βApplication ClassLoaderβ (loads application classes)
ββββββββββββββββββββββββ
π‘ Tip: The parent delegation model prevents core classes from being replacedβbefore loading a class, each loader asks its parent first!
Runtime Data Areas ποΈ
The JVM divides memory into several runtime data areas:
| Area | Scope | Purpose | GC? |
|---|---|---|---|
| Method Area | Shared | Stores class metadata, constants, static variables | Yes |
| Heap | Shared | All object instances and arrays | Yes |
| Stack | Per-thread | Local variables, method frames | No |
| Program Counter | Per-thread | Current instruction address | No |
| Native Method Stack | Per-thread | Native method calls (C/C++) | No |
Heap Memory Structure:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β HEAP MEMORY β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€ β ββββββββββββββββββββββββ βββββββββββββββββββββββ β β β Young Generation β β Old Generation β β β ββββββββββββββββββββββββ€ β (Tenured Space) β β β β Eden β S0 β S1 β β β β β β Spaceβ β β β Long-lived objects β β β β βSurvivor Spacesβ β β β β ββββββββββββββββββββββββ βββββββββββββββββββββββ β β β β β β New objects Promoted after surviving β β allocated here multiple GC cycles β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Did you know? The generational garbage collection strategy is based on the "weak generational hypothesis"βmost objects die young! By separating young and old generations, the JVM can collect garbage more efficiently.
Execution Engine β‘
The Execution Engine executes bytecode through:
- Interpreter: Executes bytecode line by line (slower but starts immediately)
- Just-In-Time (JIT) Compiler: Compiles frequently-executed bytecode to native machine code (faster execution after compilation)
- Garbage Collector: Automatically reclaims memory from unreachable objects
JIT Compilation Process:
Bytecode β Interpreter β Profiler (detects hot spots)
β
JIT Compiler
β
Native Machine Code
β
Execute (much faster!)
Garbage Collection Algorithms ποΈ
Common GC Algorithms:
- Serial GC: Single-threaded, simple, good for small applications
- Parallel GC: Multi-threaded, maximizes throughput
- CMS (Concurrent Mark-Sweep): Minimizes pause times, runs concurrently
- G1 (Garbage First): Balances throughput and latency, divides heap into regions
- ZGC/Shenandoah: Ultra-low pause times (<10ms), for large heaps
π‘ Tip: Use -XX:+PrintGCDetails to see which GC events occur and tune your application's memory settings!
Core Concepts: Design Patterns π¨
Design patterns are reusable solutions to common software design problems. The "Gang of Four" (GoF) categorized 23 patterns into three types:
| Category | Purpose | Examples |
|---|---|---|
| Creational | Object creation mechanisms | Singleton, Factory, Builder |
| Structural | Compose objects into structures | Adapter, Decorator, Proxy |
| Behavioral | Communication between objects | Strategy, Observer, Command |
Singleton Pattern π€
Ensures a class has only one instance and provides a global access point.
Use cases: Database connections, logging, configuration managers
Thread-safe implementation (Bill Pugh solution):
public class DatabaseConnection {
private DatabaseConnection() {
// Private constructor prevents instantiation
}
private static class Holder {
private static final DatabaseConnection INSTANCE = new DatabaseConnection();
}
public static DatabaseConnection getInstance() {
return Holder.INSTANCE;
}
public void connect() {
System.out.println("Connected to database");
}
}
β οΈ Common Mistake: Using simple lazy initialization without synchronization leads to multiple instances in multi-threaded environments!
Factory Pattern π
Defines an interface for creating objects but lets subclasses decide which class to instantiate.
Use cases: When you don't know the exact types of objects needed at compile time
interface Animal {
void speak();
}
class Dog implements Animal {
public void speak() {
System.out.println("Woof!");
}
}
class Cat implements Animal {
public void speak() {
System.out.println("Meow!");
}
}
class AnimalFactory {
public static Animal createAnimal(String type) {
switch (type.toLowerCase()) {
case "dog": return new Dog();
case "cat": return new Cat();
default: throw new IllegalArgumentException("Unknown animal type");
}
}
}
// Usage
Animal pet = AnimalFactory.createAnimal("dog");
pet.speak(); // Output: Woof!
π§ Try this: Extend the factory to support a "bird" type that chirps!
Builder Pattern π¨
Separates object construction from representation, allowing step-by-step creation of complex objects.
Use cases: Objects with many optional parameters (avoids telescoping constructors)
public class Computer {
// Required parameters
private final String CPU;
private final int RAM;
// Optional parameters
private final boolean hasGPU;
private final int storage;
private final String OS;
private Computer(Builder builder) {
this.CPU = builder.CPU;
this.RAM = builder.RAM;
this.hasGPU = builder.hasGPU;
this.storage = builder.storage;
this.OS = builder.OS;
}
public static class Builder {
// Required
private final String CPU;
private final int RAM;
// Optional - initialize with defaults
private boolean hasGPU = false;
private int storage = 256;
private String OS = "Windows";
public Builder(String CPU, int RAM) {
this.CPU = CPU;
this.RAM = RAM;
}
public Builder gpu(boolean hasGPU) {
this.hasGPU = hasGPU;
return this;
}
public Builder storage(int storage) {
this.storage = storage;
return this;
}
public Builder os(String OS) {
this.OS = OS;
return this;
}
public Computer build() {
return new Computer(this);
}
}
@Override
public String toString() {
return "Computer[CPU=" + CPU + ", RAM=" + RAM + "GB, GPU=" + hasGPU +
", Storage=" + storage + "GB, OS=" + OS + "]";
}
}
// Usage
Computer gamingPC = new Computer.Builder("Intel i9", 32)
.gpu(true)
.storage(1024)
.os("Windows 11")
.build();
π‘ Tip: The Builder pattern is used extensively in Java librariesβStringBuilder, Stream.Builder, and many testing frameworks!
Strategy Pattern π―
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Use cases: When you need different variants of an algorithm
interface PaymentStrategy {
void pay(int amount);
}
class CreditCardPayment implements PaymentStrategy {
private String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
public void pay(int amount) {
System.out.println("Paid $" + amount + " using credit card " + cardNumber);
}
}
class PayPalPayment implements PaymentStrategy {
private String email;
public PayPalPayment(String email) {
this.email = email;
}
public void pay(int amount) {
System.out.println("Paid $" + amount + " using PayPal account " + email);
}
}
class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
// Usage
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout(100);
cart.setPaymentStrategy(new PayPalPayment("user@email.com"));
cart.checkout(50);
Observer Pattern π
Defines a one-to-many dependency where when one object changes state, all dependents are notified.
Use cases: Event handling systems, MVC architectures, pub-sub systems
import java.util.ArrayList;
import java.util.List;
interface Observer {
void update(String message);
}
interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void setNews(String news) {
this.news = news;
notifyObservers();
}
public void attach(Observer observer) {
observers.add(observer);
}
public void detach(Observer observer) {
observers.remove(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
}
class NewsChannel implements Observer {
private String name;
public NewsChannel(String name) {
this.name = name;
}
public void update(String news) {
System.out.println(name + " received news: " + news);
}
}
// Usage
NewsAgency agency = new NewsAgency();
NewsChannel cnn = new NewsChannel("CNN");
NewsChannel bbc = new NewsChannel("BBC");
agency.attach(cnn);
agency.attach(bbc);
agency.setNews("Breaking: Design patterns are awesome!");
// Output:
// CNN received news: Breaking: Design patterns are awesome!
// BBC received news: Breaking: Design patterns are awesome!
Examples with Detailed Explanations π
Example 1: Understanding Stack vs Heap Memory
public class MemoryDemo {
public static void main(String[] args) {
int x = 10; // Primitive: stored on stack
Integer y = new Integer(20); // Object: reference on stack, object on heap
modifyPrimitive(x);
System.out.println("x = " + x); // Output: x = 10 (unchanged)
modifyObject(y);
System.out.println("y = " + y); // Output: y = 20 (unchanged, immutable)
StringBuilder sb = new StringBuilder("Hello");
modifyMutableObject(sb);
System.out.println("sb = " + sb); // Output: sb = Hello World (changed!)
}
static void modifyPrimitive(int value) {
value = 100; // Only affects local copy
}
static void modifyObject(Integer value) {
value = new Integer(200); // Reassigns local reference, doesn't affect original
}
static void modifyMutableObject(StringBuilder sb) {
sb.append(" World"); // Modifies the actual object on heap
}
}
Explanation: Primitives and references live on the stack (method frame), while objects live on the heap. When you pass arguments to methods, Java uses pass-by-valueβprimitives copy their value, objects copy their reference. Immutable objects (like Integer, String) can't be modified after creation, but mutable objects (like StringBuilder, ArrayList) can be changed through their references.
Example 2: JVM Optimization with String Pool
public class StringPoolDemo {
public static void main(String[] args) {
String s1 = "Hello"; // String literal: goes to string pool
String s2 = "Hello"; // Reuses same object from pool
String s3 = new String("Hello"); // new keyword: creates new object on heap
System.out.println(s1 == s2); // true (same reference)
System.out.println(s1 == s3); // false (different references)
System.out.println(s1.equals(s3)); // true (same content)
String s4 = s3.intern(); // Forces s3 into string pool
System.out.println(s1 == s4); // true (now references pool object)
}
}
Explanation: The JVM maintains a String Pool (in the heap's Method Area) to optimize memory usage. String literals are automatically interned, meaning identical strings share the same memory. Using new String() bypasses the pool and creates a heap object. The intern() method manually adds a string to the pool or returns the existing pooled version.
π§ Mnemonic: "Literals pool together, NEW stands apart"
Example 3: Decorator Pattern for Dynamic Behavior
interface Coffee {
String getDescription();
double getCost();
}
class SimpleCoffee implements Coffee {
public String getDescription() {
return "Simple Coffee";
}
public double getCost() {
return 2.00;
}
}
abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
public double getCost() {
return coffee.getCost() + 0.50;
}
}
class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
public double getCost() {
return coffee.getCost() + 0.25;
}
}
// Usage
Coffee myCoffee = new SimpleCoffee();
System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());
// Output: Simple Coffee $2.0
myCoffee = new MilkDecorator(myCoffee);
myCoffee = new SugarDecorator(myCoffee);
System.out.println(myCoffee.getDescription() + " $" + myCoffee.getCost());
// Output: Simple Coffee, Milk, Sugar $2.75
Explanation: The Decorator pattern allows you to add responsibilities to objects dynamically without modifying their code. Each decorator wraps the original object and adds new behavior. This is more flexible than inheritance (which is static) and follows the Open/Closed Principleβclasses should be open for extension but closed for modification. Java's InputStream hierarchy uses decorators extensively (BufferedInputStream, DataInputStream, etc.).
Example 4: Adapter Pattern for Interface Compatibility
// Target interface that client expects
interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee: existing interface that needs adaptation
interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
class VlcPlayer implements AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing VLC file: " + fileName);
}
public void playMp4(String fileName) {
// Do nothing
}
}
class Mp4Player implements AdvancedMediaPlayer {
public void playVlc(String fileName) {
// Do nothing
}
public void playMp4(String fileName) {
System.out.println("Playing MP4 file: " + fileName);
}
}
// Adapter: makes AdvancedMediaPlayer compatible with MediaPlayer
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer = new Mp4Player();
}
}
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedPlayer.playVlc(fileName);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedPlayer.playMp4(fileName);
}
}
}
class AudioPlayer implements MediaPlayer {
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing MP3 file: " + fileName);
} else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
MediaAdapter adapter = new MediaAdapter(audioType);
adapter.play(audioType, fileName);
} else {
System.out.println("Invalid format: " + audioType);
}
}
}
// Usage
AudioPlayer player = new AudioPlayer();
player.play("mp3", "song.mp3");
player.play("mp4", "video.mp4");
player.play("vlc", "movie.vlc");
Explanation: The Adapter pattern converts one interface into another that clients expect. It's like a power adapter that lets you plug a US device into a European outlet. Here, MediaAdapter wraps AdvancedMediaPlayer to make it compatible with the MediaPlayer interface. This pattern is invaluable when integrating legacy code or third-party libraries.
Common Mistakes to Avoid β οΈ
JVM Mistakes:
Memory Leaks Through Static References: Static fields live for the application's lifetime. Storing large collections in static fields prevents garbage collection.
// BAD: Memory leak public class Cache { private static List<Object> cache = new ArrayList<>(); public static void add(Object obj) { cache.add(obj); // Never removed! } }Ignoring OutOfMemoryError: Don't catch
OutOfMemoryErrorβfix the root cause (memory leaks, insufficient heap size, or inefficient algorithms).String Concatenation in Loops: Using
+in loops creates many temporary String objects.// BAD: Creates many temporary strings String result = ""; for (int i = 0; i < 1000; i++) { result += i; // Creates 1000 intermediate String objects! } // GOOD: Use StringBuilder StringBuilder sb = new StringBuilder(); for (int i = 0; i < 1000; i++) { sb.append(i); } String result = sb.toString();Premature Optimization: Don't optimize before profiling! The JIT compiler often outperforms manual optimization.
Design Pattern Mistakes:
Singleton with Public Constructor: Defeats the purposeβanyone can create new instances.
// BAD public class Singleton { private static Singleton instance = new Singleton(); public Singleton() { } // Public constructor! public static Singleton getInstance() { return instance; } }Overusing Patterns: Not every problem needs a design pattern. Patterns add complexityβuse them when the benefits outweigh the costs.
Factory Returning Concrete Types: Factories should return interfaces/abstract classes to maintain flexibility.
// BAD: Returns concrete type public Dog createAnimal() { return new Dog(); } // GOOD: Returns interface public Animal createAnimal() { return new Dog(); }Observer Memory Leaks: Forgetting to detach observers prevents garbage collection.
subject.attach(observer); // ... later ... subject.detach(observer); // Don't forget this!Builder Without Validation: Validate required fields in the
build()method.public Computer build() { if (CPU == null || RAM <= 0) { throw new IllegalStateException("CPU and RAM are required"); } return new Computer(this); }
Key Takeaways π―
β The JVM consists of class loaders, runtime data areas, execution engine, and native interfaces
β Memory is divided into heap (shared, GC-managed) and stack (per-thread, automatic)
β
String literals are pooled for memory efficiency; use intern() for manual pooling
β JIT compilation converts hot bytecode paths to native code for performance
β Garbage collection algorithms balance throughput vs. pause times; choose based on application needs
β Creational patterns (Singleton, Factory, Builder) control object creation
β Structural patterns (Adapter, Decorator, Proxy) organize object relationships
β Behavioral patterns (Strategy, Observer, Command) define object communication
β Design patterns solve recurring problems but add complexityβuse judiciously
β Understanding JVM internals helps you write more efficient code and debug production issues
Further Study π
Deepen your understanding with these resources:
Oracle's JVM Specification: https://docs.oracle.com/javase/specs/jvms/se17/html/index.html - Official documentation of JVM internals
Refactoring Guru - Design Patterns: https://refactoring.guru/design-patterns/java - Interactive visual guide to all GoF patterns with Java examples
Baeldung JVM Tutorials: https://www.baeldung.com/jvm-vs-jre-vs-jdk - Comprehensive articles on JVM performance tuning, memory management, and garbage collection
π Quick Reference Card
| JVM Memory Areas | |
|---|---|
| Heap | Objects, arrays (GC-managed, shared) |
| Stack | Local variables, method frames (per-thread) |
| Method Area | Class metadata, constants, static fields |
| Essential Design Patterns | |
| Singleton | One instance globally accessible |
| Factory | Object creation without specifying exact class |
| Builder | Step-by-step construction of complex objects |
| Strategy | Interchangeable algorithms at runtime |
| Observer | One-to-many dependency notification |
| Decorator | Add responsibilities dynamically |
| Adapter | Convert incompatible interfaces |
| JVM Tuning Flags | |
| -Xms | Initial heap size |
| -Xmx | Maximum heap size |
| -XX:+UseG1GC | Use G1 garbage collector |
| -XX:+PrintGCDetails | Print GC events |