What happens in the areas of memory managed by Java when a method is invoked?
To understand what happens when a method is invoked, we must understand some basics of Java memory management. This understanding will not only help us visualize how control passes from one method to another, and the allocation of memory for primitives and object references in parameters, local variables, and return values in a method—but also help us when we tackle (eventually) more intimidating topics, such as recursion and concurrency.
There are 5 main areas of memory managed by the Java virtual machine; for now, we will focus on these 4:
Class/method area
When a class is loaded into memory, its static
fields and the code of its methods (along with that of its constructors, static
and non-static
initializers, etc.) are loaded into this area of memory.
Heap
When an instance of any class (including any type of array) is created, the space for that instance’s state (data) is allocated in the heap as a contiguous block of memory. At certain moments (especially when a contigous block of heap memory is needed for an allocation, but no contiguous blocks of sufficient size are available), space that is no longer needed is reclaimed. This process of marking previously allocated memory as available for re-use, and moving objects around in the heap to create larger contiguous blocks of free memory for new object instances, is called garbage collection.
Stack
Each thread of executing code managed by the JVM has its own private stack, located in this area. Every method, while it is executing, has a stack frame—a block of memory on the stack, containing the parameters, local variables, and other working memory needed by the method. Each executing method has its own stack frame, which other methods can’t access in any way.
Program counter (PC) registers
The JVM maintains a program counter—a pointer to the current instruction in the code/method area—for each of the execution threads it manages. As Java bytecode instructions are executed, the associated PC register is updated.
When any method is invoked, a stack frame is pushed on (added to) the stack; this is the private working area for the method (of course, the code itself is located in the class/method area). Included in the stack frame is the space required for any arguments passed to the method, space for any local variables declared and used by the method, space for holding intermediate values used in calculations, and space holding the location in the class/method area where execution will continue when this method completes execution. The size of the stack frame needed for a method is computed at compile time, and stored in the class/method area when the class is loaded into memory. When the method is invoked, this information in the class/method space is examined by the JVM, so that a frame of the appropriate size can be pushed onto the stack.
When a method completes execution, its stack frame is popped off (removed from) the stack; the return value (if any) is written by the JVM to a location in the stack frame of the invoking method, and any other working values in the popped stack frame are lost. One implication of this is that the local variables of a method do not maintain their values across multiple invocations of that method.
One critical detail about the stack is this: When the type of a method parameter is a class (object type), or when the type of a local variable (or intermediate value) is a class, what is actually allocated in the stack frame for that parameter, variable, or intermediate value is space for a reference to such an object. When the method is invoked, and as it is executed, any such objects are allocated or referenced on the heap, with the references to those objects residing on the stack. (Arguments, local variables, and intermediate values of any of the primitive types are placed directly on the stack, in the method’s stack frame.) In fact, in Java, all variables, parameters, and fields of of object types actually hold references to such objects, rather than the object themselves.
While a method is executing, the code of that method can access only the method’s stack frame, and any objects referenced (directly or indirectly) from the stack frame. Since object references only refer to locations in the heap, there is no way for the code of a method to access the stack frame of the method that invoked the given method, or the method that invoked that one, and so on. One practical (and deliberate) consequence of this is that the local variables of a method cannot be seen or modified by any other method. Similarly, code running in any given thread is unable to access the local variables of any active method in a different thread.
When one method invokes another, the address of the next instruction in the first method is included in the data stored in the stack frame of the second. Execution of the first method is suspended while execution of the second proceeds; that is, the PC now refers to the current instruction in the second method, and is updated accordingly.
When the second method completes execution, the address previously stored in its stack frame is used to restore the PC, and execution of the first method then picks up where it left off.