How recursion works
In our introduction to recursion, we considered a simple Java example
of listing all the files on a particular disk (or section of the file system). Now we'll consider some
of the details of how recursion actually works, because we'll see that these details can have a few implications
for how we use the technique. For our example, we'll consider what happens when we recursively print
the file names from a hypothetical disk that contains the structure shown in Figure 1.
C:\
myfile.txt
picture.jpg
Programs
notepad.exe
browser.exe
Utility
chars.exe
logfile.txt
...
Figure 1. Example directory structure used for illustrating how recursion works.
In this example, the names with capital letters are directories, with indentation representing
the level in the directory tree. So notepad.exe, browser.exe and Utility are inside
Programs, and chars.exe is in turn inside Utility; logfile.txt,
with the same indentation as myfile.txt etc, is in the root of C:.
Now let's consider what happens when our recursive routine scans down this directory structure.
We initially call our listFilesInDirectory() method, passing in the root directory C:\.
Then our method calls listFiles() and gets the list of files and directories in the root of C:.
The method starts looping over this list, printing out the names of File objects that aren't
directories. All is simple, therefore, until we get to the directory Programs. At this point,
we want to make a recursive call to listFilesInDirectory(). Eventually, that call will return,
and when it returns, we need to remember "where we got to" in the list of files in C:,
and continue listing from logfile.txt.
The stack
To remember "where it got up to", the program has a stack. A stack is a special
area of memory set aside for remembering "where to go back to" every time the program makes a
method call. (Strictly speaking, every thread has its
own stack, because different threads run and make method calls independently of one another.)
Conceptually, you can imagine it like a pile of little note papers. When it needs to call
a method, the program writes a little "note" to itself, noting the values of all the
variables inside the method at the point it got up to, plus where in the method it needs to come back to.
It then places that note on top of the "pile" (the stack)1 and calls the method.
The call to list the contents of Programs will eventually get down to
the subdirectory Utility and have to make another recursive call. So again,
a "note" is placed on the stack. Once the call has gone through the contents
of Utility, the top note is pulled off the stack, telling the Java
runtime whereabouts in the processing of the Programs directory to go back to.
Then, when that directory has been completed, the top note is again pulled off
the stack. At this point, the top note is the one originally placed there before
making the call to list the contents of Programs— in other words,
we end up at the point we left off while going through the C: directory.
So to complete that loop, the file logfile.txt is processed, and the
method exits, completing the overall process.
Figure 2: Layout of local variables on the stack.
Local variables on the stack
So what actually goes on the stack? What do the "little notes" that the Java
Virtual Machine writes to itself actually look like? Well, we said that
the stack is an area of memory. When our program (or, more precisely,
our thread) starts, a fixed-size block of memory is reserved for its stack.
The computer keeps a stack pointer, which points to the top of
the stack at any one time. So to put something on top of the stack, we write it
to the memory location indicated by the stack pointer at the current moment in time,
then "nudge" the stack pointer forward by the size of the item we've just added
to the stack. To take an item back off the top of the stack, we nudge the stack
pointer backwards by the size of the item we want to take off, then read the data
from that memory location.
Now, when we call a method, an item is placed on the stack for
each local variable inside that method.
Local variables are variables that we either pass into the method
(in our case, the directory/subdirectory to be listed), or variables that
we declare inside the method (for example, we declared a
variable called files, which is the array of files inside the
directory being listed, f, which is the current file we're looking at,
and implicitly, we also had an Iterator object to iterate over the
array). Figure 2 illustrates the idea. In this case, a method (represented in blue)
was initially called, which overall had two local variables. It in turn called another
method, represented in green. Then it called a further method, represented in yellow,
which had three variables. A possible piece of code to produce this layout might
be roughly as shown in Figure 3:
void blue(int x) {
int y = 17 * x;
green(y);
}
void green(int z) {
for (int i = 0; i < z; i++) {
int val = z * i;
yellow(val);
}
}
void yellow(int x) {
int a = x * 7;
Object o = new Object();
...
}
Figure 3: Example code to produce a stack layout similar to Figure 2. For
simplicity, Figure 2 represents only the variables stored on the
stack, and not other "housekeeping" information such as the program counter.
As you reconcile the stack usage shown in Figure 2 with the code in Figure 3,
notice that, for example, in the blue() method,
both the method parameter (x) and the
variable y declared inside the method take
up space on the stack.
Notice that if a variable is an object, only the
reference is placed on the stack. The actual object data is
allocated on the Java heap, a different area of memory.
In general, each method has its own well-defined "area" on the stack,
and while a particular method is running, Java knows whereabouts on
the stack all the variables for the current method start. The Java bytecode
that is running actually refers to local variables by number,
so accessing, say, variable x actually means "access the data
from position 0 of the section of stack memory for the current method".
(When the JIT compiler translates the bytecode into native machine code, it
effectively compiles it into code that says "access the data at stack pointer
plus offset x", knowing for a given variable what the appropriate
value of x is.)
Because the stack is actually an area of memory rather than a pile of notes,
variables can often be accessed directly from the stack.
A subtlety that we haven't included in Figure 2 is that when we call
a method, we also store the current program counter: i.e.
whereabouts in the code we actually are. That way, when the called
method returns, it knows where to jump back to!
In other words:
- calling a method often means putting the parameter
values on to the stack, putting the current program counter on the stack,
moving the stack pointer on by an appropriate amount,
then jumping to that method's code;
- returning from a method is essentially the reverse
process: the old program counter is pulled back of the stack, the
stack pointer is moved back to point to the previous method's variables,
then we "jump back" and continue executing code from the location after old program
counter.
Implications for recursion
So what implications does this all have for recursion? Well, each time
we make a recursive call, we "eat up" a bit more space on the stack.
So the maximum depth of recursion is limited by:
- our thread's stack size (the amount of memory
allocated to the stack);
- the number of parameters and local variables
used on each call to our method.
1. In various languages, stack and pile are actually translated
by the same word.
If you enjoy this Java programming article, please share with friends and colleagues. Follow the author on Twitter for the latest news and rants.
Editorial page content written by Neil Coffey. Copyright © Javamex UK 2021. All rights reserved.