Cleanup:
finalization and
garbage
collection
Programmers
know about the importance of initialization, but often forget the importance of
cleanup. After all, who needs to clean up an
int?
But with libraries, simply “letting go” of an object once
you’re done with it is not always safe. Of course, Java has the garbage
collector to reclaim the memory of objects that are no longer used. Now
consider a very special and unusual case. Suppose your object allocates
“special” memory without using new.
The garbage collector knows only how to release memory allocated
with
new,
so it won’t know how to release the object’s “special”
memory. To handle this case, Java provides a method called finalize( )
that you can define for your class. Here’s how it’s
supposed
to work. When the garbage collector is ready to release the storage used for
your object, it will first call
finalize( ),
and only on the next garbage-collection pass will it reclaim the object’s
memory. So if you choose to use
finalize( ),
it gives you the ability to perform some important cleanup
at
the time of garbage collection
. This
is a potential programming pitfall because some programmers, especially C++
programmers, might initially mistake
finalize( )
for the destructor
in C++, which is a function that is always called when an object is destroyed.
But it is important to distinguish between C++ and Java here, because in C++
objects
always get destroyed
(in a bug-free program), whereas in Java objects do not always get
garbage-collected. Or, put another way:
Garbage
collection is not destruction.
If
you remember this, you will stay out of trouble. What it means is that if there
is some activity that must be performed before you no longer need an object,
you must perform that activity yourself. Java has no destructor or similar
concept, so you must create an ordinary method to perform this cleanup. For
example, suppose in the process of creating your object it draws itself on the
screen. If you don’t explicitly erase its image from the screen, it might
never get cleaned up. If you put some kind of erasing functionality inside
finalize( ),
then if an object is garbage-collected, the image will first be removed from
the screen, but if it isn’t, the image will remain. So a second point to
remember is:
Your
objects might not get garbage collected.
You
might find that the storage for an object never gets released because your
program never nears the point of running out of storage. If your program
completes and the garbage collector never gets around to releasing the storage
for any of your objects, that storage will be returned to the operating system
en
masse
as the program exits. This is a good thing, because garbage collection has some
overhead, and if you never do it you never incur that expense.
What
is finalize( ) for?
You
might believe at this point that you should not use
finalize( )
as a general-purpose cleanup method. What good is it?
A
third point to remember is:
Garbage
collection is only about memory.
That
is, the sole reason for the existence of the garbage collector is to recover
memory that your program is no longer using. So any activity that is associated
with garbage collection, most notably your
finalize( )
method, must also be only about memory and its deallocation.
Does
this mean that if your object contains other objects
finalize( )
should explicitly release those objects? Well, no – the garbage collector
takes care of the release of all object memory regardless of how the object is
created. It turns out that the need for
finalize( )
is limited to special cases, in which your object can allocate some storage in
some way other than creating an object. But, you might observe, everything in
Java is an object so how can this be?
It
would seem that
finalize( )
is in place because of the possibility that you’ll do something C-like by
allocating memory using a mechanism other than the normal one in Java. This can
happen primarily through
native
methods
,
which are a way to call non-Java code from Java. (Native methods are discussed
in Appendix A.) C and C++ are the only languages currently supported by native
methods, but since they can call subprograms in other languages, you can
effectively call anything. Inside the non-Java code, C’s
malloc( )
family of functions might be called to allocate storage, and unless you call
free( )
that storage will not be released, causing a memory leak. Of course,
free( )
is a C and C++ function, so you’d need call it in a native method inside
your
finalize( ). After
reading this, you probably get the idea that you won’t use
finalize( )
much. You’re correct; it is not the appropriate place for normal cleanup
to occur. So where should normal cleanup be performed?
You
must perform cleanup
To
clean up an object, the user of that object must call a cleanup
method at the point the cleanup is desired. This sounds pretty straightforward,
but it collides a bit with the C++ concept of the destructor.
In C++, all objects are destroyed. Or rather, all objects
should
be
destroyed. If the C++ object is created as a local, i.e. on the stack (not
possible in Java), then the destruction happens at the closing curly brace of
the scope in which the object was created. If the object was created using
new
(like in Java) the destructor is called when the programmer calls the C++
operator
delete
(which doesn’t exist in Java). If the programmer forgets, the destructor
is never called and you have a memory leak, plus the other parts of the object
never get cleaned up.
In
contrast, Java doesn’t allow you to create local objects – you must
always use
new.
But in Java, there’s no “delete” to call for releasing the
object since the garbage collector releases the storage for you. So from a
simplistic standpoint you could say that because of garbage collection, Java
has no destructor. You’ll see as this book progresses, however, that the
presence of a garbage
collector does not remove the need or utility of destructors. (And you should
never call finalize( )
directly, so that’s not an appropriate avenue for a solution.) If you
want some kind of cleanup performed other than storage release you must
still
call a method in Java, which is the equivalent of a C++ destructor without the
convenience.
One
of the things
finalize( )
can be useful for is observing the process of garbage collection. The following
example shows you what’s going on and summarizes the previous
descriptions of garbage collection:
//: Garbage.java
// Demonstration of the garbage
// collector and finalization
class Chair {
static boolean gcrun = false;
static boolean f = false;
static int created = 0;
static int finalized = 0;
int i;
Chair() {
i = ++created;
if(created == 47)
System.out.println("Created 47");
}
protected void finalize() {
if(!gcrun) {
gcrun = true;
System.out.println(
"Beginning to finalize after " +
created + " Chairs have been created");
}
if(i == 47) {
System.out.println(
"Finalizing Chair #47, " +
"Setting flag to stop Chair creation");
f = true;
}
finalized++;
if(finalized >= created)
System.out.println(
"All " + finalized + " finalized");
}
}
public class Garbage {
public static void main(String[] args) {
if(args.length == 0) {
System.err.println("Usage: \n" +
"java Garbage before\n or:\n" +
"java Garbage after");
return;
}
while(!Chair.f) {
new Chair();
new String("To take up space");
}
System.out.println(
"After all Chairs have been created:\n" +
"total created = " + Chair.created +
", total finalized = " + Chair.finalized);
if(args[0].equals("before")) {
System.out.println("gc():");
System.gc();
System.out.println("runFinalization():");
System.runFinalization();
}
System.out.println("bye!");
if(args[0].equals("after"))
System.runFinalizersOnExit(true);
}
} ///:~
The
above program creates many
Chair
objects, and at some point after the garbage collector begins running, the
program stops creating
Chairs.
Since the garbage collector can run at any time, you don’t know exactly
when it will start up, so there’s a flag called
gcrun
to indicate whether the garbage collector has started running yet. A second flag
f
is a way for
Chair
to tell the
main( )
loop that it should stop making objects. Both of these flags are set within
finalize( ),
which is called during garbage collection.
Two
other
static
variables,
created
and
finalized,
keep track of the number of
objs
created versus the number that get finalized by the garbage collector. Finally,
each
Chair
has its own (non-
static)
int
i
so it can keep track of what number it is. When
Chair
number 47 is finalized, the flag is set to
true
to bring the process of
Chair
creation
to a stop.
All
this happens in
main( ),
in the loop
while(!Chair.f) {
new Chair();
new String("To take up space");
}
You
might wonder how this loop could ever finish, since there’s nothing
inside that changes the value of
Chair.f.
However, the
finalize( )
process will, eventually, when it finalizes number 47.
The creation of a
String
object during each iteration is simply extra garbage being created to encourage
the garbage collector to kick in, which it will do when it starts to get
nervous about the amount of memory available.
When
you run the program, you provide a command-line argument of
“before” or “after.” The “before” argument
will call the
System.gc( )
method (to force execution of the garbage collector) along with the
System.runFinalization( )
method to run the finalizers. These methods were available in Java
1.0, but the runFinalizersOnExit( )
method that is invoked by using the “after” argument is available
only in Java 1.1[19]
and beyond. (Note you can call this method any time during program execution,
and the execution of the finalizers is independent of whether the garbage
collector runs).
The
preceding program shows that, in Java 1.1, the promise that finalizers will
always be run holds true, but only if you explicitly force it to happen
yourself. If you use an argument that isn’t “before” or
“after” (such as “none”), then neither finalization
process will occur, and you’ll get an output like this:
Created 47
Beginning to finalize after 8694 Chairs have been created
Finalizing Chair #47, Setting flag to stop Chair creation
After all Chairs have been created:
total created = 9834, total finalized = 108
bye!
Thus,
not all finalizers get called by the time the program completes.
[20]
To force finalization to happen, you can call
System.gc( )
followed by
System.runFinalization( ).
This will destroy all the objects that are no longer in use up to that point.
The odd thing about this is that you call
gc( )
before
you call
runFinalization( ),
which seems to contradict the Sun documentation, which claims that finalizers
are run first, and then the storage is released. However, if you call
runFinalization( )
first, and then
gc( ),
the finalizers will not be executed.
One
reason that Java 1.1
might default to skipping finalization for all objects is because it seems to
be expensive. When you use either of the approaches that force garbage
collection you might notice longer delays than you would without the extra
finalization.
[19]
Unfortunately,
the implementations of the garbage collector in Java 1.0 would never call
finalize( )
correctly. As a result,
finalize( )
methods that were essential (such as those to close a file) often didn’t
get called. The documentation claimed that all finalizers would be called at
the exit of a program, even if the garbage collector hadn’t been run on
those objects by the time the program terminated. This wasn’t true, so as
a result you couldn’t reliably expect
finalize( )
to be called for all objects. Effectively,
finalize( )
was useless in Java 1.0.
[20]
By the time you read this, some Java Virtual Machines may show different
behavior.