A Journey Through Java Execution: From the Loader to the Memory Model

Have you ever wondered what happens behind the scenes when you press the “run” button on your Java program? The process involves a series of complex steps, from compiling and loading the code into memory to managing the data in data structures such as heap and stack.

Here, we’ll explore the steps involved in running a basic Java program, highlighting the roles of the loader, compiler, launcher, and memory model. Consider a simple Java program that calculates the factorial of the 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 compiling, loading and running this program and also understand the memory management aspect of this program.

Program example: 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 

1. Compiler: performance architect

Our journey begins with the compiler. Java Compiler (javac) translates our readable Java code into bytecode that the JVM can understand.

When we publish javac Factorial.java command, Java compiler (javac) translates Factorial.java source code to bytecode.

Compiler - performance engineering

During compilation, the compiler checks for syntax errors and checks the code’s type safety, ensuring that data types are used correctly and consistently throughout the code. Any errors or inconsistencies are flagged, allowing the developer to modify the code.

2. Loader: Making 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 starting a Java program, the loader retrieves the bytecode from the classpath, including Factorial class. This bytecode is a platform-independent representation of our code.

Loader - preparing the environment for execution

In a simple Java program such as Factorial for example, utility classes from the Java Standard Library, such as those in the java.lang the package can be loaded into memory along with Factorial class.

Some of the utility classes from java.lang The package that can be loaded initially includes:

  1. Object Class: Every class in Java implicitly extends Object class, so that it is loaded into memory when the JVM starts. The Object class provides underlying methods such as equals, hashCodeand toString.
  2. String Class: The String class is often used in Java programs to manipulate strings. It is loaded into memory to support operations involving strings.
  3. System Class: The System class provides access to system properties and I/O streams. Often used for console input/output, environment variables, and system-related operations.
  4. Math Class: The Math class provides mathematical functions and constants. It is usually used for arithmetic calculations in Java programs.
  5. ClassLoader Class: The ClassLoader class is responsible for dynamically loading Java classes into the JVM. Although the program may not reference this class directly, it is included in the class loading process.

Utility classes

These utility classes are essential to the basic functioning of a Java program and are automatically loaded into memory by the JVM via the loader along with the Factorial class. They provide the core functionality commonly used in Java applications.

3. Runner: Starting the action

As the program runs, the JVM coordinates execution by taking on the role of “runner”. The main method acts as an entry point, signaling the start of the action.

Memory management, exception handling, and method calls occur seamlessly.

Memory management, exception handling, and method calls occur seamlessly.

  • Objects and their instance variables are allocated on the heap. Method calls and local variables find their place on the stack.
  • Each thread in the application has its own stack frame, providing private space for method calls and data storage.
  • In the sample program, after calling the main method, a new stack frame appears on the stack, with local variables such as n and result.
  • The factorial a method is called which initiates a series of recursive calls and the creation of successive stack frames.
  • With each recursive call, the stack is filled with new frames, each of which encapsulates method parameters and local variables.
  • As method calls return, the stack frames are smoothly retracted, passing control back to their predecessors.
  • Finally, when the main method finishes running, the Java program terminates.

4. Model of memory: behind the scenes

Let’s go a little deeper into memory management to understand memory allocation.

Behind the scenes, the Java memory model governs how data is stored and accessed at runtime. The heap, a shared pool of memory, holds dynamically allocated objects, while the stack provides dedicated space for method calls and local variables.

During execution, memory plays a key role in the coordination of 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 factorial method runs, stack frames are created and popped from the stack, reflecting the dynamic nature of method invocations.

Although there are none in our case, objects and their instance variables, if any, find their place on the heap.

But why are the objects assigned space in the pile, and why not on the stack? Let’s discuss it.

Objects are allocated on the heap as they are created at runtime and are dynamic. Objects created in Java can have different lifetimes specified using the scope identifiers “public”, “private”, “protected” and “private to package” (the default). This extends their lifetime beyond the scope of the methods or blocks in which they were created. Placing objects on the heap allows them to persist beyond a single method call, allowing them to be accessed from multiple parts of the program.

Model of memory

But references to those objects are usually stored on the stack or inside other objects on the heap.

This reference is essentially a memory address that points to the object’s location on the heap. So while the actual data about an object resides on the heap, a reference to that object is stored on the stack.

While object references are usually stored on the stack, it is important to note that objects can also be referenced directly from the heap. This happens 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 is 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 arises in main() method, reference to MyClass the object is stored on the stack.

When obj1.setAnotherObject(new AnotherClass()) it’s called, an AnotherClass the object is created on the heap and its reference is stored inside MyClass object in the stack.

Seamless stack-to-stack interactionSo while objects are allocated on the heap, references to those objects are usually stored on the heap or within other objects on the heap, depending on the context in which they are used.

The seamless interaction between the heap and the heap ensures the efficient allocation and deallocation of memory, facilitating the seamless execution of our Java program.

Conclusion

As our journey through Java execution draws to a close, we reflect on the elaborate interaction of the loader, compiler, loader, and memory model that drives 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 magic behind the scenes 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

A must read for continued learning

Source link

Leave a Reply

Your email address will not be published. Required fields are marked *