Ethan is Java Evangelist and Ed is chief technology officer for the KL Group. They can be contacted at [email protected] and [email protected], respectively.
One of Java's biggest selling points has been its supposed immunity to one of the most challenging programming problems -- memory leaks. But some Java developers have observed their Java programs exhibit classic memory-leak behavior -- unbounded memory growth leading to poor performance and eventually crashing. What's going on?
First, let's look at how dynamic memory management works in Java and understand what the garbage collector does. Objects are allocated on the heap using the new operator and accessed via references. Probably the easiest way to think about memory in Java is to picture the heap forming a directed graph, where objects form the nodes and the references between objects make the edges. The garbage collector sees the memory this way, as a graph of objects and references.
The purpose of the garbage collector is to remove from memory objects that are no longer needed. This is a hard problem to solve -- the garbage collector can't tell whether you need a particular object, so it uses an approximation and looks for objects that are no longer reachable. Using the directed graph analogy, it looks for objects that can't be reached by any path starting from a root. Roots, fixed places that are always guaranteed to exist, are the starting points for the garbage collector. In Java, the roots include static fields in classes and locals on the stack. Anything that the garbage collector can't reach from one of the program's roots by any path is considered garbage.
To illustrate this, look at Example 1 and Figure 1. The method has two local references on the stack, m1 and m2. There's also a variable created outside the scope of this method called global. m1 and m2 are, temporarily at least, two roots for the garbage collector. Two objects are created and two references, or edges, are created to those objects (from the locals on the stack). Another reference is added from m1 to m2 and a reference is added from the global object. When the method returns, m1 and m2 are no longer on the stack, so the first object that was created is no longer reachable. Because the garbage collector can no longer reach that object by some path it will, at some point in the future, clean up that object. It's important to note that garbage collection does not happen immediately, but on a periodic basis. Even though the object will stay in memory for some period of time until the garbage collector releases it, it remains unreachable and can't be reused.
There are some common myths about garbage collection in Java that are worth cleaning up. The first one is that the garbage collector can't handle cycles -- it can. That is, if you have three objects -- A, B, and C -- with references from A to B, B to C, and C to A, and those are the only references to those objects, the garbage collector will clean those objects up. This is in contrast to other systems that use reference counting techniques (such as Microsoft's COM), which do have problems handling cycles in the object reference graph.
The second myth, and this is really for people who've moved to Java from C++, is that the finalizer is the same as a C++ destructor -- it isn't. There are a number of subtle differences, but the most important one is that the finalizer is not guaranteed to be called, unlike a destructor in C++, which is explicitly called in order to remove an object. You can't reliably depend on the finalizer in Java. One interesting piece of trivia, however, is that if the finalizer is called, it's possible for it to resurrect the object, by making a reference to the object that's about to be garbage collected from another object, thus making it reachable again. While this is a bad thing to do in practice, the garbage collector is aware of the fact that it can, in theory, happen.
Loiterers
Now that we've talked about what the garbage collector is and what it does, let's look at what it means to have a memory leak in Java. As Figure 2 illustrates, there are three states that an object can be in:
- Allocated objects are all objects that have been created but not yet removed by the garbage collector.
- Reachable objects are all the allocated objects that can be reached from one of the roots.
- Live objects are reachable objects that are being actively used by your program.
The garbage collector takes care of objects that are allocated but unreachable. In contrast, these objects would be memory leaks in C++, memory that's permanently lost to the program. Tools like Rational's Purify and Numega's BoundsChecker are designed to help track down this kind of problem in C++, finding objects that are allocated but no longer reachable.
In Java, the situation is different. The garbage collector takes care of the allocated but unreachable objects for you, so a Java memory leak is instead an object that's reachable but not live. Even though you have a reference to that object somewhere and there's a path to that object from some root, the object isn't needed by the program and could be disposed of -- if there wasn't a reference to it.
So one contrast between memory leaks in C++ and Java is that in C++ once you leak an object, the problem can't be fixed by the program -- there are no remaining references to that object. In Java, the object itself can be reached, but the code that manages the object may not be accessible to you; for example, the reference to the unneeded object might be from a private field in a class for which you don't have the source code. On the other hand, if the reference itself is accessible, then there should be some action the program can take to remove all the references to the objects making it unreachable and eligible for garbage collection.
Another difference, going back to the analogy of viewing the heap as a directed graph of objects and references, is that in C++ you have to manage both the nodes and the edges. Every time you add or remove objects or references, you're changing the collection of both nodes and edges. If you leave some edges hanging, by freeing an object without removing all the pointers to that object, you get a dangling pointer, which usually results in something like Windows' infamous GPF error. Conversely, if you leave a node hanging, by removing all the pointers without removing the node, you have a memory leak. In Java, you can only do the second of these two things, removing the edges. Ultimately, you only have control over the references, so you have to think about managing just the edges. If you don't remove references to objects, the garbage collector can't remove them. You have to assist the garbage collector by managing the edges.
One thing that we have found in investigating memory leaks in Java is that they are rarer than they are in C++. In C++, it's easy to get a memory leak by not writing destructors for classes or not bothering to free memory on the heap. But in Java, the garbage collector does a lot of this work for you. The flip side to this is that the impact of memory leaks, the amount of memory that's being lost, tends to be much greater in Java.
The reason is that when you have an object that's not being used any more, it's rarely the case that there's just a single object. That object will have references to other objects, which will have more references, and so on, forming a large subgraph of objects that are leaked, just because one reference wasn't properly cleared. For example, Swing or AWT programming containers (such as panels or frames) include other child components (buttons, text fields, and the like). The container can reach all of its children as it has references to them (to lay them out). At the same time, each component has a reference back to its parent. There is, therefore, a path from every object in the user interface to every other object. Compounding the problem, UI objects are often subclassed, adding additional references and objects into the subgraph. The result is that the memory leak is not just a small set of components, it can be a very large collection of objects that's leaking.
Since there are many distinct differences between memory leaks in C++ and Java, it's confusing to use the same term to refer to both of them. Therefore, we refer to these unused objects in Java as "loiterers." The dictionary definitions of a loiterer are "to delay an activity with aimless idle stops and pauses" (which will happen as the garbage collector has more and more objects to check on each pass) and "to remain in an area for no obvious reason" (you're not using them, so why are they there?) -- both fairly apt descriptions of what's going on. Another good reason to use a different term is that the Java Virtual Machine and many of the libraries have native code in them, written in C++, and that code may have memory leaks in it, leading to confusion as to whether a leak is in Java code or C++ code that's underneath the Java.
Lexicon of Loiterers
To further clarify and understand how loiterers occur, we've identified four different patterns of loitering objects (and you may see a theme here):
Lapsed Listeners. A lapsed listener is when an object is added to a collection but never removed. The most common example of this is an event listener, where the object is added to a listener list, but never removed once it is no longer needed. So the object's usefulness has lapsed because although it's still in the list, receiving events, it no longer performs any useful function. One of the side effects of this is that the collection of listeners may be growing without bound. You can keep adding listeners to a collection, but they are never removed. This causes the program to slow down as events have to be propagated to more and more listener objects, causing each event to take longer and longer to process. This is probably the most common memory-usage problem in Java--Swing and AWT are very susceptible to this problem and it can occur easily in any large framework. For example, see bug #4177795 in the Java Developer's Connection (at http://developer .java.sun.com/ developer/bugParade/index .html). In this case, instances of the javax.swing.JInternalFrame class were loitering if a menu bar had been added to them. Through a long series of events, it turned out that the hashtable that keeps track of all keystrokes registered for menu shortcuts was holding onto a reference to the menu, which was holding onto the internal frame, preventing any of these objects from being garbage collected, even after all the references from inside the program were removed. It's surprisingly easy to create this kind of problem.
In contrast, this kind of problem rarely occurs in a C++ program. The memory would probably be freed without removing the pointer from the list, creating a dangling pointer. When the program walks through the list and tries to dispatch the event via the bad pointer, the program would probably crash. Whether it's better to leak memory or to crash is for you to decide.
Another example of a lapsed listener in Java 2 is a method on java.awt.Toolkit called addPropertyChangeListener(). You can register a listener there to receive notification whenever any desktop properties change, such as the resolution of the desktop. Because the Toolkit class is a Singleton, there's only ever one instance of it that is created at the start of the application and survives for the lifetime of the entire application. Most listeners, however, are going to have much shorter life spans. If you have a reference from something that has a long life span to something that has a short life span, then the short-lived object is now going to live much longer, as the reference from the long-lived object will keep it around indefinitely. You have to remember to call removePropertyChangeListener() whenever the listener object is destroyed. This isn't really when the listener is literally destroyed, as the garbage collector does that -- it's when you decide that the listener object is no longer needed by the program.
Some strategies you can use to avoid lapsed listeners are to make sure all the add and remove calls are paired. Doing this is as simple as using tools such as grep or the find command in your favorite editor to search for calls to addXXXListener and removeXXXListener. Furthermore, it's good practice to pair them close together in your code and not to have the add and remove listener calls spread far apart in separate methods or source code files. At some point in the future the calls are going to get decoupled and you're going to create a loitering object problem again. Another thing, shown in the example, is to pay attention to object lifecycles -- creating references from a long-lived object to a short-lived object ties both objects together, giving them the long-lived object's lifetime. Finally, you might want to consider a larger solution, such as implementing a listener registry or a publish/ subscribe mechanism, to decouple listeners from even sources. You should be suspicious of any framework code that claims to clean up this sort of problem automatically, as it's probably built on a set of assumptions that, if broken, will cause the framework to fail and possibly cause more loiterers.
Lingerers. The second type of loiterer is a lingerer -- an object that hangs on for a while, after the program is finished with it. Specifically, it occurs when a reference is used transiently by a long-lived object, but isn't cleared when finished with. The next time the reference is used it will probably be reset to refer to a different object, but in the meantime, the previous object loiters about. In C++, this would again be a benign dangling pointer, where the object being referenced would have been manually freed and the bad pointer would have been retained, but you'd never notice, as the next time the pointer is used, it will be reset to point to some other valid object.
An example of this might be a print service in an application (see Example 2). The print service can be implemented as a Singleton, as there isn't usually any need to have multiple print services in an application. The print service contains a field called target. When the program calls doPrint(), the print service prints the object referred to by target. The important thing is that when the print service is done printing, the target reference is not set to null. The object that was being printed can't be garbage collected now, as there's still a lingering reference to it from the printer object. You have to make sure that transient references are set to null once you've finished using them.
One strategy for dealing with lingerers is to encapsulate state in a single object as opposed to having a number of objects maintaining state information. This makes changing state easier, as there's only one reference to deal with. In general, lingerers often occur when objects with multiple states hold on to references unnecessarily when they're in a quiescent or inactive state, so you have to carefully consider the state-based behavior of your objects. Another strategy is to avoid early exits in methods -- you should set up methods so that they do their setup first, the processing, and finally any necessary clean up. If you exit before the method has a chance to clean up, references may be left holding on to objects that are no longer needed.
Laggards. The third type of loiterer is a laggard -- someone (or something) who is always behind, never quite keeping up. In terms of loiterers, a laggard occurs when an object changes its state, but still has references to some data from its old state. Laggards are typically functional errors in addition to memory problems, but they're often hard to find and may manifest themselves as memory problems before they're discovered as bugs. One way that laggards occur is when you change the lifecycle of a class; for example, when you change a class from having multiple instances to a Singleton, perhaps because it's too expensive to keep creating new objects of this class. Now the single object of this class changes its state over time, as opposed to before when new instances were created whenever a new state was required. Again, comparing the situation to C++, this problem would probably manifest itself as a dangling pointer in C++, where the objects from the old state would have been manually removed, leaving a bad pointer.
An example of this might be an object that maintains information about files in a directory, including statistics and which has references to the largest, smallest, and "most complex" file (for some definition of "complex"). When you change directories, for some reason only the references to the largest and smallest files are updated -- the reference to the most complex file is a laggard, as it still points to the file in the previous directory. This is, of course, a bug, but it's subtle and may be difficult to detect. Using a memory debugging tool, however, where you can see all the instances of each class, you should be able to see that there are more references to file objects than there are files in the directory because of the extra file being held on to by the laggard reference. Approaching this problem as a lingerer as opposed to a bug may make it show up much more quickly.
You can deal with laggards by thinking carefully about your caching strategies: Is caching really necessary or is it acceptable to calculate certain values dynamically? It's useful to use a profiler to determine when and where caching is appropriate. Another technique is to encapsulate state transitions in a single method, so you don't have code scattered in multiple locations responsible for changing the state of an object. Keeping related code in a single locality makes it easier to maintain.
Limbo. The fourth and final type of loiterer is a limbo. Things in limbo are caught in between two places, while occupying neither of them fully. Objects in limbo may not be long-term loiterers, but they can take up a lot of memory at times when you don't want them to. Limbos occur when an object being referenced from the stack is pinned in memory by a long running thread. The problem is that the garbage collector can't do what's referred to as "liveness analysis" where it would be able to find out that an object won't be used anywhere in the rest of a method, thus making it eligible for garbage collection.
In Example 3, the method is supposed to read through a file, parse items out of it, and deal with certain elements in it. This might happen if you were looking for a specific piece of data in an XML file, for instance. So the first thing the method does is call readIt(), which might do something like read in the whole file, which would consume a lot of memory. Then the method findIt() goes through and searches for the particular information you're looking for, condensing all the information from the big object into something much smaller. From this point on you don't need big any more and you'd probably like to reuse the memory it's occupying. But when you call parseIt(), which may take a long time, the memory for big can't be reused because there's still a reference to it from the stack in method()'s stack frame -- big can't be garbage collected until method() returns. You need to help the garbage collector out by setting the reference to big to null, as shown in Example 3 in the line that's commented out.
One way to deal with limbos is to be aware of long-running methods and watch where large allocations are occurring, to make sure that you're not creating large objects that are being held on the heap by a reference on the stack. Again, tools such as profilers and memory debuggers can help determine what methods take a long time to run and what objects are very large. Explicitly adding statements to set references to null in cases where large objects are being needlessly held can make a big difference. While it's not practical or necessary to null out every reference after you're done with it, it helps where appropriate. A blocked thread can also be a problem; for example, when a thread is blocked waiting on I/O, no object referenced from the stack in that thread can be garbage collected.
Tools and Techniques
There are a number of tools available to help you track down loiterers. One simple thing to do is to track the objects you're creating manually so that you can programmatically monitor memory usage. The problem with this is, of course, that you have to modify your code in order to see what's going on. An example of how to do this is demonstrated by ObjectTracker.java (Listing One), a class that lets you register objects to track them to see if you have the expected number of instances. Listing Two is an example of using ObjectTracker. To activate ObjectTracker, you have to define the ObjectTracker system property by adding the command-line flag "-DObjectTracker" when you run the Java VM.
One important thing to note is that successful use of ObjectTracker relies on the Java VM assigning all objects unique hashcodes. Unfortunately, due to differences in implementation, this is only guaranteed to be true in Sun's JDK 1.1.x JVMs and not in JDK 1.2 or higher. ObjectTracker will appear to work in JDK 1.2.x (or the 1.3 beta), but may not accurately track large numbers of objects.
A more industrial-strength solution is to use a full-blown profiler and/or memory debugger. There are a number of commercial products available, including JProbe (http://www.klgroup.com/jprobe/) from KL Group (where we work). These types of commercial products can track all the objects in your program, let you browse the heap and, very importantly, see not only the objects but also the references between them -- this is important because in Java you have to worry about managing the references (the edges of the graph formed by objects on the heap), and not the nodes.
Along the same line, there are some freeware tools available that make use of the profiling output available from JDK 1.2.x JVMs. The -Xrunhprof option (explained by running java -classic -Xrunhprof:help) can generate both time and memory usage information. This data can, in turn, be interpreted by tools such as HyperProf (http:// www.physics.orst.edu/~bulatov/HyperProf/ index.html/) although this seems to have been removed by the author for software patent reasons. The data format produced by -Xrunhprof is documented in the output file and on Sun's web site (http://developer .java.sun.com/developer/onlineTraining/ Programming/JDCBook/perf3.html/).
One final possibility is to use the Java Virtual Machine Profiling Interface directly (documented at http://java.sun.com/ products/jdk/1.2/docs/guide/jvmpi/jvmpi .html). It can be used to monitor a number of different internal activities inside the VM, such as object allocation and removal (see "What Is the Java VM Profiler Interface," by Andy Wilson, DDJ September 1999). The major drawback with using JVMPI is that it's a native interface and you'll have to create your own C-based shared object library or DLL to get the information. While this is definitely the most flexible approach, it's no small amount of work and it's probably cheaper in the long run to buy (or better yet, get your boss to buy) a commercial memory-debugging tool. A free tool that uses JVMPI is JUM (http://www.iro.umontreal.ca/~lelouarn/ jum.html).
Conclusion
While Java's garbage collection mechanism removes much of the difficulty of managing dynamic memory, problems can still occur. Most nontrivial Java programs will have some loitering objects present in them. Loitering objects are generally fewer than memory leaks in C++, but when they do occur they generally cause much larger problems. Removing loitering objects can be difficult because Java handles memory in a fundamentally different way than C++, which is what most developers are familiar with. You have to think about managing the edges, not the nodes on the heap. A thorough understanding of object lifecycles, including lifetimes and state values, is key and must be used to build good memory-management practices into development practices.
DDJ
Listing One
/***************************************************************************** * Copyright (c) 1999, KL GROUP INC. All Rights Reserved. * http://www.klgroup.com * The Software is provided "AS IS," without a warranty of any kind. * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. * KL GROUP AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING * THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL KL GROUP OR ITS * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, * HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT * OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF KL GROUP HAS * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. *****************************************************************************/ import java.lang.reflect.*; import java.util.*; /************************************************************* * Utility class for identifying loitering objects. Objects are tracked by * calling ObjectTracker.add() when instantiated, and calling * ObjectTracker.remove() when finalized. Only classes that implement * ObjectTracker.Tracked can be tracked. As instances are created and * destroyed, they are reported to the stdout. Summaries by class can also be * reported on demand. To enable this functionality, add -DObjectTracker * when running your program. This will track all classes that implement * ObjectTracker.Tracked and call add/remove as indicated in the * previous paragraph. * For a finer degree of control, specify a list of filters * when setting the <code>ObjectTracker</code> property. For instance, * -DObjectTracker=+MySpecialClass,-ClassFoo will only report o * on instances of classes whose name contains MySpecialClass * but not ClassFoo. Hence MySpecialClassBar will be tracked, while * MySpecialClassFoo will not be. See <A HREF="ObjectTracker.html#start()"> * start()</A> for more details. * Limitations * Since you must add instrumentation to all the classes you want to track, * this is not nearly as useful as a Memory Profiler/Debugger like * JProbe Profiler. Also, since it cannot tell you which references * are causing the object to loiter, it doesn't help you remove the loiterers. * If you want to solve the problem, you really need to use a Memory * Profiler/Debugger like JProbe Profiler. The only thing ObjectTracker can * help with is testing whether an instance of a known class goes away. * Implementation Notes * The current implementation assumes that every object has a unique * hashcode. A false assumption in general, but does work in JavaSoft's Win32 * VM for JDK1.1. This implementation will definitely not work in JavaSoft's * implementation of the Java 2 VM, including the HotSpot VM. ************************************************************* */ public class ObjectTracker { // Property ObjectTracker turns this on when set private final static boolean ENABLED = System.getProperty("ObjectTracker") != null; // Classes are hashed by name into this table. private static Hashtable classReg; private static Vector patterns; /** Record info about an object. Class and ordinal number are stored. */ private static class ObjectEntry { int ordinal; // distinguishes between mult. instances String clazz; // classname String name; // name (may be null) public ObjectEntry(int ordinal, String clazz, String name) { this.ordinal = ordinal; this.clazz = clazz; this.name = name; } public String toString() { return clazz + ":#" + ordinal + " ("+name+")"; } } // ObjectEntry /** Records info about a class. Within each class, a table of objects is * maintained, along with next ordinal to use to stamp next object * of this class. */ private static class ClassEntry { String clazz; // class name Hashtable objects; // list of ObjectEntry int ordinal; // last instance of this class created public ClassEntry(String clazz) { this.clazz = clazz; objects = new Hashtable(); ordinal = 1; } public String toString() { return clazz; } /** Get the name of the object by invoking getName(). * Uses reflection to find the method. */ private String getName(Object o) { String name = null; try { Class cl = o.getClass(); Method m = cl.getMethod("getName", null); name = (m.invoke(o, null)).toString(); } catch (Exception e) { } return name; } public void addObject(Object obj) { // Store this object in the object table Integer id = new Integer(System.identityHashCode(obj)); ObjectEntry entry = new ObjectEntry(ordinal, clazz, getName(obj)); objects.put(id, entry); ordinal++; System.out.println(" added: " +entry); } public void removeObject(Object obj) { // Removes this object from the object table Integer id = new Integer(System.identityHashCode(obj)); ObjectEntry entry = (ObjectEntry) objects.get(id); objects.remove(id); System.out.println(" removed: " +entry); } /** Dump out a list of all object in this table */ public void listObjects() { if (objects.size() == 0) { // skip empty tables return; } System.out.println("For class: " + clazz); Enumeration objs = objects.elements(); while (objs.hasMoreElements()) { ObjectEntry entry = (ObjectEntry) objs.nextElement(); System.out.println(" " +entry); } } } // ClassEntry /** No constructor */ private ObjectTracker() {} /** Determine is this class name should be tracked. * @return true if this class should be tracked. @see start */ private static boolean isIncluded(String clazz) { int i=0, size = patterns.size(); if (size == 0) { // always match if list is empty return true; } boolean flag = false; for (; i<size; i++) { String pat = (String) patterns.elementAt(i); String op = pat.substring(0, 1); // + or - String name = pat.substring(1); if (name.equals("all")) { if (op.equals("+")) flag = true; // match all, unless told otherwise else if (op.equals("-")) flag = false; // match nothing, unless told otherwise } else if (clazz.indexOf(name) != -1) { // match if any of the filter names is a substring of // the class name if (op.equals("+")) return true; else if (op.equals("-")) return false; } } return flag; } /** Must be called before any objects can be tracked. Turns on object tracking * if property <code>ObjectTracker</code> is set. In addition, the list of * patterns assigned to this property is stored for future pattern matching * by <code>isIncluded()</code>. This list of patterns must be supplied as a * comma-separated list, each preceded by <code>+</code> or <code>-</code>, * which indicates whether or not the pattern should cause matching classes to * be tracked or not. If property <code>ObjectTracker</code> has no values, * it is equivalent to <code>+all</code>. */ public static void start() { if (ENABLED) { classReg = new Hashtable(); patterns = new Vector(); String targets = System.getProperty("ObjectTracker"); StringTokenizer parser = new StringTokenizer(targets, ","); while (parser.hasMoreTokens()) { String token = parser.nextToken(); patterns.addElement(token); } } } /** Add object to the tracked list. Will only be added if object's class has * not been filtered out. @param obj object to be added to tracking list */ public static void add(Tracked obj) { if (ENABLED) { String clazz = obj.getClass().getName(); if (isIncluded(clazz)) { ClassEntry entry = (ClassEntry) classReg.get(clazz); if (entry == null) { // first one for this class entry = new ClassEntry(clazz); classReg.put(clazz, entry); } entry.addObject(obj); } } } /** Removes object from tracked list. This method should be called * from the finalizer. @param obj object to be removed from tracking list */ public static void remove(Tracked obj) { if (ENABLED) { String clazz = obj.getClass().getName(); if (isIncluded(clazz)) { ClassEntry entry = (ClassEntry) classReg.get(clazz); entry.removeObject(obj); } } } /** Print tracked objects, summarized by class. Also prints a * summary of free/total memory. */ public static void dump() { if (ENABLED) { Enumeration e = classReg.elements(); while (e.hasMoreElements()) { ClassEntry entry = (ClassEntry) e.nextElement(); entry.listObjects(); } System.out.println("=================================="); System.out.println("Total Memory: " + Runtime.getRuntime().totalMemory()); System.out.println("Free Memory: " + Runtime.getRuntime().freeMemory()); System.out.println("=================================="); System.out.println(""); } } /** All classes that want to use this service must implement this * interface. This forces this class to implement Object's finalize * method, which should call <code>ObjectTracker.remove()</code>. */ public interface Tracked { /** All classes that use ObjectTracker must implement a finalizer. */ void finalize(); } }
Listing Two
/*************************************************************************** * Copyright (c) 1999, KL GROUP INC. All Rights Reserved. * http://www.klgroup.com * The Software is provided "AS IS," without a warranty of any kind. * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. * KL GROUP AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES * SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING * THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL KL GROUP OR ITS * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, * HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT * OF THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF KL GROUP HAS * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ***************************************************************************/ public class tester implements ObjectTracker.Tracked { private int[] junk = new int[5000]; public static void main(String args[]) { ObjectTracker.start(); for (int i=0; i<1000; i++) { tester t = new tester(); t.doNothing(); if (i%100 == 0) { System.gc(); } } ObjectTracker.dump(); } public tester() { ObjectTracker.add(this); } public void finalize() { ObjectTracker.remove(this); } public void doNothing() { } }