Journey Through Java Execution: From Loader to Memory Model
This article explores the steps of running a basic Java program, highlighting the roles of the loader, compiler, runner, and memory models.
Join the DZone community and get the full member experience.
Join For FreeEver wondered what happens behind the scenes when you hit that "run" button on your Java program? The process involves a series of complex steps, from compiling and loading the code into memory to managing data in data structures like the heap and stack.
Here, we'll explore the steps of running a basic Java program, highlighting the roles of the loader, compiler, runner, and memory model. Consider a simple Java program that calculates the factorial of a number "n" using recursion.
As you know, the Factorial of a number is defined by the recurrence relation:
F (n) = n * F (n-1) with base cases F (0) = 1 and F (1) = 1
Let’s go through the various steps of compilation, loading, and running of this program and also understand the memory management aspect of this program.
Example Program: Factorial Calculation
public class Factorial {
public static void main(String[] args) {
int n = 5;
int result = factorial(n);
System.out.println("Factorial of " + n + " is: " + result);
}
public static int factorial(int n) {
if (n == 0 || n == 1) {
return 1;
} else {
return n * factorial(n - 1);
}
}
}
1. The Compiler: Architect of Performance
Our journey begins with the Compiler. The Java compiler (javac
) translates our human-readable Java code into bytecode that the JVM can understand.
When we issue the javac Factorial.java
command, the Java compiler (javac
) translates the Factorial.java
source code into bytecode.
During compilation, the compiler checks for syntax errors and verifies the type safety of the code ensuring that data types are used correctly and consistently throughout the code. Any mistakes or inconsistencies are flagged, allowing the programmer to modify the code.
2. The Loader: Crafting the Stage
With the stage set, it's time for the loader, a vital component of the Java Virtual Machine (JVM) to take center stage. When launching a Java program, the loader fetches bytecode from the classpath, including the Factorial
class. This bytecode is a platform-independent representation of our code.
In a simple Java program like the Factorial
example, utility classes from the Java standard library, such as those in the java.lang
package may be loaded into memory along with the Factorial
class.
Some of the utility classes from the java.lang
package that may be loaded initially includes:
Object
Class: Every class in Java implicitly extends theObject
class, so it is loaded into memory when the JVM starts. TheObject
class provides fundamental methods such asequals
,hashCode
, andtoString
.String
Class: TheString
class is frequently used in Java programs for manipulating strings. It is loaded into memory to support operations involving strings.System
Class: TheSystem
class provides access to system properties and I/O streams. It is often used for console input/output, environment variables, and system-related operations.Math
Class: TheMath
class provides mathematical functions and constants. It is commonly used for arithmetic calculations in Java programs.ClassLoader
Class: TheClassLoader
class is responsible for dynamically loading Java classes into the JVM. While the program may not directly reference this class, it is involved in the class loading process.
These utility classes are essential for a Java program's basic functioning and are automatically loaded into memory by the JVM through the Loader along with the Factorial
class. They provide foundational functionality that is commonly used in Java applications.
3. The Runner: Initiating Action
As the program unfolds, the JVM coordinates the execution taking on the “runner” role. The main method acts as the entry point, signaling the start of the action.
Memory management, exception handling, and method invocations occur seamlessly.
- Objects and their instance variables are allocated on the heap. Method invocations and local variables find their place on the stack.
- Each thread in the application has its own stack frame, providing a private space for method calls and data storage.
- In the sample program, upon invocation of the main method, a new stack frame emerges on the stack, housing local variables such as
n
andresult
. - The
factorial
method is called, triggering a series of recursive calls and the creation of successive stack frames. - With each recursive invocation, the stack swells with new frames, each encapsulating the method's parameters and local variables.
- As the method calls return, stack frames retreat smoothly, passing control back to their predecessors.
- In the end, when the main method finishes running, the Java program ends.
4. The Memory Model: Behind the Scenes
Let’s go a little deeper into the memory management to understand the memory allocation.
Behind the scenes, the Java memory model governs how data is stored and accessed during execution. The heap, a shared pool of memory, accommodates dynamically allocated objects, while the stack provides a dedicated space for method invocations and local variables.
Throughout the execution, memory plays a pivotal role in coordinating the execution.
In our sample program, primitive variables like n
and result
find their place on the stack, within their respective stack frames.
As recursive calls to the factoria
l method unfold, stack frames are created and popped off the stack, reflecting the dynamic nature of method invocation.
Although absent in our case, objects and their instance variables if any, find their place in the heap.
But, why are objects allocated space in the heap, and why not on the stack? Let’s discuss that.
Objects are allocated on the heap as they are created during the run-time and are dynamic. Objects created in Java may have varying lifetimes specified using the scope identifier “public”, “private”, "protected," and "package-private" (default). This extends their lifetime beyond the scope of the methods or blocks in which they are created. Placing objects on the heap allows them to persist beyond the invocation of a single method, enabling them to be accessed by multiple parts of the program.
But references to those objects are typically stored on the stack or within other objects on the heap.
This reference is essentially a memory address that points to the location of the object in the heap. So, while the actual object data resides in the heap, the reference to that object is stored on the stack.
While references to objects are typically stored on the stack, it's important to note that objects can also be referenced directly from the heap. This occurs when one object contains a reference to another object as one of its instance variables. In this case, the reference to the second object is stored within the memory allocated for the first object on the heap.
Here's an example to illustrate both scenarios:
public class Example {
public static void main(String[] args) {
// Creating an object and storing its reference on the stack
MyClass obj1 = new MyClass();
// Creating another object and storing its reference in the instance variable of the first object
obj1.setAnotherObject(new AnotherClass());
}
}
class MyClass {
private AnotherClass anotherObject;
public void setAnotherObject(AnotherClass obj) {
this.anotherObject = obj;
}
}
class AnotherClass {
// Class definition
}
In this example:
When obj1
is created in the main()
method, a reference to the MyClass
object is stored on the stack.
When obj1.setAnotherObject(new AnotherClass())
is called, an AnotherClass
object is created on the heap, and its reference is stored within the MyClass
object in the heap.
So, while objects are allocated on the heap, references to those objects are typically stored on the stack or within other objects on the heap, depending on the context in which they are used.
The seamless interaction between heap and stack ensures efficient memory allocation and deallocation, facilitating the flawless execution of our Java program.
Conclusion
As our journey through Java execution comes to a close, we reflect on the elaborate interaction of the loader, compiler, runner, and memory model that powers every Java program. From loading code into memory to managing data structures, the JVM ensures flawless execution of Java programs.
So, the next time you run a Java program, take a moment to appreciate the behind-the-scenes magic that makes it all possible. And as the applause rings out, remember the journey from loader to memory model that paved the way for your code to shine.
Video
Must Read for Continuous Learning
- Head First Design Patterns
- Clean Code: A Handbook of Agile Software Craftsmanship
- Java Concurrency in Practice
- Java Performance: The Definitive Guide
- Designing Data-Intensive Applications
- Designing Distributed Systems
- Clean Architecture
- Kafka — The Definitive Guide
- Becoming An Effective Software Engineering Manager
Published at DZone with permission of Roopa Kushtagi. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments