Java Main Method & JVM Memory Model
Table of Contentsβ
1. The main Method Deep Diveβ
Why is main Special?β
The main
method is the entry point of any standalone Java application. When you run java MyClass
, the JVM specifically looks for this exact signature:
public static void main(String[] args)
If missing: JVM throws Error: Main method not found in class MyClass
Breakdown of Each Keywordβ
1. public
β
- Purpose: JVM must access it from outside the class
- Impact: If
private
orprotected
β runtime error
// β This won't work
private static void main(String[] args) {
System.out.println("Hello World");
}
// JVM cannot access private methods
2. static
β
- Purpose: JVM calls main without creating class instance
- Impact: Belongs to class, not instance
// β
JVM can call directly
public class MyClass {
public static void main(String[] args) {
// No need for: MyClass obj = new MyClass();
System.out.println("Hello World");
}
}
3. void
β
- Purpose: main doesn't return anything
- Impact: JVM just exits after execution
// β JVM wouldn't know what to do with return value
public static int main(String[] args) {
return 42; // What should JVM do with this?
}
4. main
β
- Purpose: Method name JVM is hardcoded to find
- Impact: Execution always starts from main
5. String[] args
β
- Purpose: Holds command-line arguments
- Variations allowed:
String args[]
String... args
(varargs)
public class Demo {
public static void main(String[] args) {
System.out.println("Args length: " + args.length);
if (args.length > 0) {
System.out.println("First arg: " + args[0]);
}
}
}
// Run: java Demo hello world
// Output:
// Args length: 2
// First arg: hello
Method Overloading with mainβ
public class MainOverload {
// β
JVM will call this one
public static void main(String[] args) {
System.out.println("Main with String[] args");
main("custom"); // Can call overloaded version
}
// β
Valid overload, but JVM won't call it directly
public static void main(String arg) {
System.out.println("Overloaded main: " + arg);
}
}
2. JVM Execution Processβ
Complete Flow: From Source to Executionβ
Your Code (.java)
β javac (compilation)
Bytecode (.class)
β java (execution)
JVM β Class Loader β Bytecode Verifier β Execution Engine
β
Calls public static void main(String[] args)
β
Your Program Runs π
Step-by-Step Processβ
Step 1: Compilationβ
javac MyClass.java
# Creates MyClass.class (platform-independent bytecode)
Step 2: Execution Launchβ
java MyClass hello world
# Launches JVM with command-line arguments
Step 3: Class Loadingβ
// Class Loader Subsystem loads classes in this order:
// 1. BootStrap Loader β core Java classes (java.lang.String)
// 2. Extension Loader β extended libraries
// 3. Application Loader β your classes (MyClass)
Step 4: JVM Verificationβ
- Bytecode Verifier checks for:
- Legal bytecode instructions
- No memory access violations
- Type safety compliance
Step 5: Execution Engineβ
- Interpreter: Executes instructions line by line
- JIT Compiler: Converts frequently used code to native machine code
Step 6: Finding & Calling mainβ
// JVM searches for exact signature
public static void main(String[] args)
// If found, JVM calls:
MyClass.main(new String[]{"hello", "world"});
3. JVM Memory Areasβ
Memory Structure Overviewβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β JVM Memory β
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
β βMethod Area β β Heap β βStack(Thread)β β
β β(Metaspace) β β β β β β
β β- Class Info β β- Objects β β- Local Vars β β
β β- Static Varsβ β- String Poolβ β- Method β β
β β- Bytecode β β- Arrays β β Frames β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Detailed Memory Areasβ
1. Method Area (Metaspace in Java 8+)β
Stores: Class-level data
- Class definitions and metadata
- Static variables and methods
- Method bytecode
- Constant pool
2. Heapβ
Stores: Objects and instance data
- All object instances
- Instance variables
- Arrays
- String Pool (for string literals)
3. Stack (Per Thread)β
Stores: Method execution data
- Local variables
- Method parameters
- Return addresses
- References to heap objects
Memory Example with Codeβ
public class MemoryDemo {
private static int staticVar = 100; // Method Area
public static void main(String[] args) {
int x = 10; // Stack
String s1 = "Hello"; // String Pool (Heap)
String s2 = new String("World"); // Heap
Person p = new Person("John"); // Heap
processData(x, s1);
}
static void processData(int num, String text) {
// New stack frame created
int result = num * 2; // Stack
System.out.println(text + ": " + result);
}
}
class Person {
private String name; // Instance variable (Heap)
public Person(String name) {
this.name = name;
}
}
Memory Layout for Above Codeβ
Method Area:
βββ MemoryDemo class metadata
βββ Person class metadata
βββ staticVar = 100
βββ Method bytecodes (main, processData, Person constructor)
Heap:
βββ String Pool: "Hello"
βββ new String("World") object
βββ Person object { name: "John" }
βββ String object "John" (for Person's name)
Stack (main thread):
βββββββββββββββββββββββ
β processData() frame β
β - num = 10 β
β - text β "Hello" β
β - result = 20 β
βββββββββββββββββββββββ
βββββββββββββββββββββββ
β main() frame β
β - args[] β
β - x = 10 β
β - s1 β "Hello" β
β - s2 β new "World" β
β - p β Person object β
βββββββββββββββββββββββ
Stack Frame Lifecycleβ
public class StackExample {
public static void main(String[] args) {
System.out.println("1. main() starts - frame pushed");
methodA();
System.out.println("4. Back in main() - methodA frame popped");
}
static void methodA() {
System.out.println("2. methodA() starts - frame pushed");
methodB();
System.out.println("3. Back in methodA() - methodB frame popped");
}
static void methodB() {
System.out.println("3. methodB() executing - top frame");
}
}
// Stack Evolution:
// Step 1: [main()]
// Step 2: [main()] β [methodA()]
// Step 3: [main()] β [methodA()] β [methodB()]
// Step 4: [main()] β [methodA()]
// Step 5: [main()]
4. Multithreading & Memory Modelβ
Thread Memory Isolationβ
Key Principle: Each thread gets its own stack, but all threads share the same heap and method area.
Thread-1 Stack Thread-2 Stack Shared Memory
βββββββββββββββ βββββββββββββββ βββββββββββββββ
βLocal vars β βLocal vars β β Heap β
βMethod framesβ βMethod framesβ β Objects β
β β β β β β
βββββββββββββββ βββββββββββββββ βββββββββββββββ€
βMethod Area β
β Static vars β
βββββββββββββββ
Thread Safety Exampleβ
public class ThreadSafetyDemo {
private static int sharedCounter = 0; // Shared in Method Area
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
// Each thread has its own stack with local variables
for (int i = 0; i < 1000; i++) { // 'i' is thread-local
sharedCounter++; // RACE CONDITION - shared resource
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
// Expected: 2000, Actual: Often less due to race condition
System.out.println("Final Counter: " + sharedCounter);
}
}
Race Condition Problemβ
// What happens during sharedCounter++:
// 1. READ current value from memory
// 2. INCREMENT the value
// 3. WRITE back to memory
// If both threads execute simultaneously:
Thread-1: READ (0) β INCREMENT (1) β WRITE (1)
Thread-2: READ (0) β INCREMENT (1) β WRITE (1)
// Result: 1 instead of 2 (lost update)
Solutions to Race Conditionsβ
Solution 1: Synchronized Blockβ
public class SynchronizedDemo {
private static int counter = 0;
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { // Only one thread at a time
counter++;
}
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter: " + counter); // Always 2000
}
}
Solution 2: AtomicIntegerβ
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
counter.incrementAndGet(); // Atomic operation
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final Counter: " + counter.get()); // Always 2000
}
}
Memory Model Summaryβ
Memory Area | Access | Thread Safety | Contains |
---|---|---|---|
Stack | Per-thread | Thread-safe | Local variables, method parameters |
Heap | Shared | Needs synchronization | Objects, instance variables |
Method Area | Shared | Needs synchronization | Static variables, class metadata |
Key Takeawaysβ
- Local variables are automatically thread-safe (stored in individual stacks)
- Shared objects in heap require synchronization
- Static variables are shared across all threads
- Race conditions occur when multiple threads access shared mutable state
- Synchronization mechanisms (synchronized, atomic classes) ensure thread safety
Quick Referenceβ
Main Method Checklistβ
- β
public static void main(String[] args)
- β Exact signature required by JVM
- β Entry point of application
- β Can be overloaded but JVM calls String[] version
Memory Areasβ
- ποΈ Method Area: Class definitions, static variables
- π Heap: Objects, instance variables (shared)
- π Stack: Local variables, method frames (per-thread)
Thread Safety Rulesβ
- π Stack variables: Thread-safe automatically
- β οΈ Heap objects: Need synchronization
- π¨ Static variables: Need synchronization