Thinking in Java - 4th Edition
1079 pág.

Thinking in Java - 4th Edition

Disciplina:Programação Orientada a Objetos1.264 materiais32.168 seguidores
Pré-visualização50 páginas
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 for or the utility of destructors.
(And you should never call finalize( ) directly, so that’s not a solution.) If you want some
kind of cleanup performed other than storage release, you must still explicitly call an
appropriate method in Java, which is the equivalent of a C++ destructor without the
convenience.

Remember that neither garbage collection nor finalization is guaranteed. If the JVM isn’t
close to running out of memory, then it might not waste time recovering memory through
garbage collection.

The termination condition

In general, you can’t rely on finalize( ) being called, and you must create separate “cleanup”
methods and call them explicitly. So it appears that finalize( ) is only useful for obscure
memory cleanup that most programmers will never use. However, there is an interesting use
of finalize( ) that does not rely on it being called every time. This is the verification of the
termination condition4 of an object.

At the point that you’re no longer interested in an object—when it’s ready to be cleaned up—
that object should be in a state whereby its memory can be safely released. For example, if
the object represents an open file, that file should be closed by the programmer before the
object is garbage collected. If any portions of the object are not properly cleaned up, then you
have a bug in your program that can be very difficult to find. finalize( ) can be used to
eventually discover this condition, even if it isn’t always called. If one of the finalizations
happens to reveal the bug, then you discover the problem, which is all you really care about.

Here’s a simple example of how you might use it:

//: initialization/TerminationCondition.java
// Using finalize() to detect an object that
// hasn’t been properly cleaned up.
class Book {
 boolean checkedOut = false;
 Book(boolean checkOut) {
 checkedOut = checkOut;
 }

                                                            
4 A term coined by Bill Venners (www.Artima.com) during a seminar that he and I were giving together.

 void checkIn() {
 checkedOut = false;
 }
 protected void finalize() {
 if(checkedOut)
 System.out.println("Error: checked out");
 // Normally, you’ll also do this:
 // super.finalize(); // Call the base-class version
 }
}
public class TerminationCondition {
 public static void main(String[] args) {
 Book novel = new Book(true);
 // Proper cleanup:
 novel.checkIn();
 // Drop the reference, forget to clean up:
 new Book(true);
 // Force garbage collection & finalization:
 System.gc();
 }
} /* Output:
Error: checked out
*///:~

The termination condition is that all Book objects are supposed to be checked in before they
are garbage collected, but in main( ), a programmer error doesn’t check in one of the books.
Without finalize( ) to verify the termination condition, this can be a difficult bug to find.

Note that System.gc( ) is used to force finalization. But even if it isn’t, it’s highly probable
that the errant Book will eventually be discovered through repeated executions of the
program (assuming the program allocates enough storage to cause the garbage collector to
execute).

You should generally assume that the base-class version of finalize( ) will also be doing
something important, and call it using super, as you can see in Book.finalize( ). In this
case, it is commented out because it requires exception handling, which we haven’t covered
yet.

Exercise 10: (2) Create a class with a finalize( ) method that prints a message. In
main( ), create an object of your class. Explain the behavior of your program.

Exercise 11: (4) Modify the previous exercise so that your finalize( ) will always be
called.

Exercise 12: (4) Create a class called Tank that can be filled and emptied, and has a
termination condition that it must be empty when the object is cleaned up. Write a
finalize( ) that verifies this termination condition. In main( ), test the possible scenarios
that can occur when your Tank is used.

How a garbage collector works

If you come from a programming language where allocating objects on the heap is expensive,
you may naturally assume that Java’s scheme of allocating everything (except primitives) on
the heap is also expensive. However, it turns out that the garbage collector can have a
significant impact on increasing the speed of object creation. This might sound a bit odd at
first—that storage release affects storage allocation—but it’s the way some JVMs work, and it

122 Thinking in Java Bruce Eckel

means that allocating storage for heap objects in Java can be nearly as fast as creating storage
on the stack in other languages.

For example, you can think of the C++ heap as a yard where each object stakes out its own
piece of turf. This real estate can become abandoned sometime later and must be reused. In
some JVMs, the Java heap is quite different; it’s more like a conveyor belt that moves
forward every time you allocate a new object. This means that object storage allocation is
remarkably rapid. The “heap pointer” is simply moved forward into virgin territory, so it’s
effectively the same as C++’s stack allocation. (Of course, there’s a little extra overhead for
bookkeeping, but it’s nothing like searching for storage.)

You might observe that the heap isn’t in fact a conveyor belt, and if you treat it that way,
you’ll start paging memory—moving it on and off disk, so that you can appear to have more
memory than you actually do. Paging significantly impacts performance. Eventually, after
you create enough objects, you’ll run out of memory. The trick is that the garbage collector
steps in, and while it collects the garbage it compacts all the objects in the heap so that you’ve
effectively moved the “heap pointer” closer to the beginning of the conveyor belt and farther
away from a page fault. The garbage collector rearranges things and makes it possible for the
high-speed, infinite-free-heap model to be used while allocating storage.

To understand garbage collection in Java, it’s helpful learn how garbage-collection schemes
work in other systems. A simple but slow garbage-collection technique is called reference
counting. This means that each object contains a reference counter, and every time a
reference is attached to that object, the reference count is increased. Every time a reference
goes out of scope or is set to null, the reference count is decreased. Thus, managing
reference counts is a small but constant overhead that happens throughout the lifetime of
your program. The garbage collector moves through the entire list of objects, and when it
finds one with a reference count of zero it releases that storage (however, reference counting
schemes often release an object as soon as the count goes to zero). The one drawback is that
if objects circularly refer to each other they can have nonzero reference counts while still
being garbage. Locating such self-referential groups requires significant extra work for the
garbage collector. Reference counting is commonly used to explain one kind of garbage
collection, but it doesn’t seem to be used in any JVM implementations.

In faster schemes, garbage collection is not based on reference counting. Instead, it is based
on the idea that any non-dead object must ultimately be traceable back to a reference that
lives either on the stack or in static storage. The chain might go through several layers of
objects. Thus, if you start in the stack and in the static storage area and walk through all the
references, you’ll find all the live objects. For each reference that you find, you must trace
into