There is a widespread belief that because Java provides "garbage collection" the programmer automatically avoids the memory management problems that plague the users of other languages. This opinion continues to exist despite there being plenty of material that attempts to correct this impression - and the existence of tools to address memory management problems.
The typical Java developer of my experience either doesn't know there is a problem or, more rarely, knows there is a problem but doesn't know how to address it. While it may be that I'm stretching a point to class this as the "orthodox" view I still feel justified in addressing the topic because it is far better not to create a mess than to have to sort one out.
Part of the problem stems from the treatment of the topic in numerous introductory texts. These tend to present this uncritical viewpoint of "garbage collection" - and early beliefs are always the hardest to challenge. I can remember realising that there was something wrong with the way I was taught about the lifecycle of Java objects. The books I had read about Java told me that their life ended when they were destroyed by the garbage collector. For example in Java in a Nutshell we have:
The technique Java uses to get rid of objects once they are no longer needed is called garbage collection. It is a technique that has been around for years in languages such as Lisp. The Java interpreter knows what objects it has allocated. It can also figure out which variables refer to which objects, and which objects refer to which other objects. Thus, it can figure out when an allocated object is no longer referred to by any other object or variable. When it finds such an object, it knows that it can destroy it safely, and does so. The garbage collector can also detect and destroy "cycles" of objects that refer to each other, but are not referred to by any other objects. [Flanagan]
Or alternatively in Exploring Java we have:
Now that we've seen how to create objects, it's time to talk about their destruction. If you're accustomed to programming in C or C++, you've probably spent time hunting down memory leaks in your code. Java takes care of object destruction for you; you don't have to worry about memory leaks, and you can concentrate on more important programming tasks. [Niemeyer-Peck97]
In all object-oriented systems the design of object lifecycles is important because objects have responsibilities to meet at significant points in their lives. If an object is being destroyed then it must ensure that any resources that it owns are either released or passed on to a new owner. And sure enough, Exploring Java continues with:
Before a method is removed by garbage collection, its finalize() method is invoked to give it a last opportunity to clean up its act and free other kinds of resources it may be holding. While the garbage collector can reclaim memory resources, it may not take care of things like closing files and terminating network connections very gracefully or efficiently. That's what the finalize() method is for. [Niemeyer-Peck97]
When I first read this it all seemed to make sense, but while working with Java, I became increasingly conscious that reality didn't accord with this view. For example, when working with instances of the AWT Graphics class I learnt very quickly that I needed to call dispose - and not to rely on the object to do so when it was destroyed.
I'm clearly not the only one to see that there is a problem. It is common to see advice like "only put debug code in the finalize method" (which directly contradicts the last quote). One of the books that tries to address the problem is Thinking in Java which says:
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. [Eckel98]
Over time I accumulated an assorted collection of rules of thumb that dealt with most circumstances. But they lacked conceptual elegance and when new circumstances occurred they required new rules to be worked out carefully.
Then one day I was reading something by Bjarne Stroustrup about the use of garbage collection. Stroustrup wasn't writing about Java (he's the creator of C++) but, despite what you may have heard in some of the Java texts, garbage collection is available to C++ programmers. What Stroustrup said made sense of these rules:
Garbage collection can be seen as a way of simulating an infinite memory in a limited memory. With this in mind we can answer a common question: Should a garbage collector call the destructor for an object it recycles? The answer is no, because an object placed on the free store and never deleted is never destroyed. [Stroustrup91]
This realisation bound all my rules of thumb together as a single idea: in Java objects are immortal - they are never destroyed. All that should happen in garbage collection is that the memory "owned" by the object is recycled. While important for the application as a whole this isn't a significant event in the object's lifecycle - and we shouldn't expect the object to respond in any significant way. (Which is consistent with the difficulty of writing effective finalize methods - which Stroustrup also alludes to later in the same passage.)
The idea of an object continuing to exist without its memory may sound a little strange - but objects exist without other resources that make them useful (a Graphics object still exists after its native peer has been released by calling dispose). In any case, the rules of garbage collection ensure that a program cannot tell if the object is there or not. While from the point of view of designing my programs I find the idea that the object is going to sit there forever holding onto any resources I haven't told it to release compelling.
One of the important points taken up by the "Patterns" movement of software design is that a "solution" has consequences. With any design decision there are tradeoffs: resolving one problem may make others worse or even introduce new ones. When the designers of Java adopted garbage collection to manage memory they didn't provide a solution to all the resource management problems a developer will ever encounter. Nor did they believe that they had:
Garbage collection (GC) is probably the most widely misunderstood feature of the Java platform. GC is typically advertised as removing all memory management responsibility from the application developer. This just isn't the case. On the other hand, some developers bend over backwards trying to please the collector, and often wind up doing much more work than is required. A solid understanding of the garbage collection model is essential to writing robust, high-performance software for the Java platform. [JPAppGC]
There are memory management problems that Java's garbage collection based model for object lifetimes doesn't solve, but nothing is harder to fix than a problem people won't believe in. And the orthodox belief that "you don't need to worry about memory leaks" denies the obvious - memory is a finite resource that needs to be managed.
The following will explore the "infinite memory/immortal objects" design model of the object lifecycle and the way in which it helps to deal with the management of resources in a Java program.
The next section "Garbage Collection and the Object Lifecycle" provides necessary detail of how garbage collection operates for discussing the problems that it does solve and those that are left for the programmer to address.
In "Managing Memory" we will be looking at the memory management problems that programmers can still encounter and the solutions to them. (The solutions are easy once you realise that the problems and solutions exist.)
In "Managing Other Resources" we will examine the management of other resources. In many programming languages (and C++ is often cited in the literature) these can be dealt with by the same mechanisms as managing memory. However, in Java, the commonality in the way these problems are addressed is less obvious - garbage collection addresses a lot of memory management issues but leaves other resource management issues to the developer.
In this section I explain what "garbage collection" does for the developer and why it fails to be the "silver bullet" that many think it is. With this knowledge we will be then equipped to tackle the problems of managing both memory and other resources that may be associated with an object.
In abstract terms "garbage collection" is a service provided by the Java runtime environment to reclaim memory from objects that the program is not going to use again. How does it know which objects are not going to be used again? It doesn't - if it knew instantly, and with complete accuracy, which objects are not going to be used then the orthodox view would be valid. But, even in theory, it is not possible to achieve complete accuracy in a useful timeframe. Instead garbage collection follows rules that can quickly identify objects that definitely won't be used again.
Although the details of how the runtime environment works out which objects the program can or cannot access depend upon the implementation of Java, there are certain rules that it must follow. (I cannot give a single reference for these rules as they are spread around the Java Language Specification and the JVM specification and are not always stated explicitly.) These rules tell the runtime environment which objects the program cannot use - the trouble is that they can sometimes indicate that an object is "in use" when the programmer has forgotten all about it and will, in practice, never use it again. The object is dead but still consuming resources - such "zombie objects" can lead to a program consuming more and more memory until it fails (by slowing to a crawl or by crashing).
The rules the collector has to follow are as these:
-
There are some references referred to as the "root set" (I'll get back to this) - objects referenced by these are in use.
-
Any object referenced by a reference in an object that is in use is also in use. (This is obviously recursive.)
-
Don't collect memory from any object that is in use. There is no requirement to collect memory from objects that are not in use.
-
Before collecting memory from an object call its finalize method - remembering that finalize might set a reference somewhere to the object that changes it to be in use.
-
Don't call finalize on the same object twice.
The definition of the "root set" is key to the working of the collector and has changed subtly since the early days of Java (as users of the SINGLETON anti-pattern may have discovered). But for the programmer it suffices to assume that it includes any objects whose methods are active on a call stack, those referenced by local variables on a call stack, and by static member variables. (Actually the latter are not really in the root set - they are "in use" indirectly, by way of the corresponding class and classloader objects - they could become unused if the class was not loaded by the default classloader.)
It is significant that there is no requirement to collect memory from objects that are not in use. There is no guarantee that an object's memory will be collected - or that finalize will ever be called. There is a deprecated API - System.runFinalizersOnExit - that purports to ensure that all finalizers will be called, but this has proved problematic:
This method is inherently unsafe. It may result in finalizers being called on live objects while other threads are concurrently manipulating those objects, resulting in erratic behavior or deadlock. [JDK1.4.1]
Because one can never be sure that a Java object will be finalised or have its memory collected it is never a good idea (as already noted) to put any functional code in a finalizer. And, unless its finalizer contains functional code, once an object becomes eligible for collection, it will have no further effect on the state of the program - it can be forgotten.
Because comparisons are often drawn with C++ it is worth expanding on this point of difference between the lifecycle of a C++ object and that of a Java object. A typical C++ object has a lifecycle like that of a Java variable of primitive type (like an int): it is created where it is declared and no longer exists after the program leaves the scope in which it is declared. In C++ objects are notified when their life comes to an end (a special destructor "method" is called). In C++ programmers use this to free resources when an object goes out of scope[1]. The result of this is that the lifecycle of a C++ object has a clear and predictable beginning (it is created) and a clear and predictable end (it is destroyed). In Java objects also have a clear and predictable beginning but they don't have a clear end: instead the program just stops using them - after which they may be finalised, after which their memory may be collected.
Instead of dying (like a C++ object) it has attained a form of immortality - but this is at the cost of the programmer needing to ensure that it frees up any resources it might be holding when she finishes with it.
When a programmer needs an object she gets it from somewhere (usually by creating it using new), probably stores a reference to it in a local variable, uses it for something (typically by calling some methods on it), and then she forgets about it. This is a very natural way to behave - we all do it unless we have had a reason to learn to do otherwise. Parents will recognise this as the way young children treat objects in the real world: they pick up a toy (or a piece of cutlery), make use of it for a while and then forget about it. Once a child has forgotten a toy it won't be long before they want to use their hands for something else and the toy will be left unattended. Eventually, a parent acts and, having decided that the object isn't being used will either send a message to the child ("pick that up and put it away!") or collect and deal with it.
Programmers (and children) get an important benefit from having things cleared up for them: it makes their life a lot simpler. Most of the time this is good - it allows them time to concentrate on matters that are important (most Java programmers are trying to solve problems, not to manage memory). The Java programmers I know were obviously children once and learnt some of the same strategies. But there are differences: instead of holding objects in their hands they now have reference variables. Children run out of hands, while programmers can endlessly create more variables, which means that there is no practical limit to the number of forgotten objects that a programmer might be "carrying around".
Java garbage collection is also different to most parents: it will never ask the programmer to "put that away" it simply deals with those objects that have been left lying around. Children don't like being nagged and neither do programmers, so this might seem great - except that putting things away is sometimes necessary. My youngest son recently negotiated a "no nagging" deal for his bedroom and has demonstrated that it becomes unusable in about a week - and he has no idea how to resolve this problem. He tried putting a couple of things away, but that didn't look any better - so he gave up.
Programs can get into the same state as that bedroom if the programmer relies on the garbage collector to magically take care of everything. Java developers who believe that you don't have to worry about memory management cause more problems than does the need to do it "by hand" in C or C++. These programs might function correctly for a while, but they use increasing amounts of memory until they collapse. Once this happens I've seen developers make a token effort at addressing the problem and then give up in the hope that it won't matter.
This doesn't need to be the case: there are idiomatic ways to manage memory in Java - it is ignorance of these idioms that causes problems, not the language. Java's garbage collection is a tool for managing memory - not a substitute. Garbage collection isn't unique to Java and, despite it frequently being cited as an advantage over C++ one may choose to use "garbage collection" in C++ but, by deliberate intent or accident, most developments in these languages don't use it. Each language provides a context and the developer must learn the idioms that work in that context.
In Java the programmer needs to ensure that there are no "live" references to objects that are no longer in use. This doesn't come naturally - the lessons learned in childhood are not automatically transferred into the made-up world of the JVM. There are parallels: in the real world children have to learn to behave responsibly with the objects they come in contact with, in the JVM world programmers have to learn to behave responsibly with the objects they come in contact with. The difference is that in the real world children learn from adults whereas in JVM world programmers usually learn from other programmers. (Science fiction writers such as William Golding [Golding] have speculated on the effect that a lack of adult input might have on children.) Why do these differences arise? In part it is because in the made-up world of the JVM we are freed from the constraints that arise in the real world. In part it is because the rules of the JVM world have been devised for the convenience of the implementers of that world.
Programmers are not stupid (and neither are children) but they are in the business of producing simplified descriptions of things (usually represented in code). Sometimes, however, they come up with descriptions that are too simple to work. One such description is a lifecycle of a Java object that goes: "creation (using new or createInstance), use (by accessing its methods or member variables) and forget about it (garbage collection will sort it out)". This is similar to a child in the real world: "create a game (by finding some toys), use (play with them) and forget about it (parents will sort them out)". In the real world parents will soon indicate that there is some learning to do, in the JVM world programmers need to discover this for themselves.
How does all this affect us as programmers? Well, the first thing that is clear is that if we have any references to unwanted objects then those objects will lurk around in limbo. This is quite easy to do unintentionally: all we have to do put the object into a collection and forget to remove it, or into a long lived variable and forget to reset it, or...
The answer is simple: ensure that any long-lived references are set to null when the object they reference is no longer in use. The problem the programmer has to address is deciding when use of an object is complete.
References that exist in function scope are rarely a problem: unless the code is written in a perverse style (e.g. excessively long methods) then the scope of the reference will be approximately the scope of use for the corresponding object. When this happens the reference will go out of scope in a timely manner and the object will become eligible for collection.
Instance reference members live as long as the instance that contains them - which can be a very long time (e.g. a SINGLETON lives "forever"). The problem for the developer implementing the class is that it is the user of the class that controls its lifetime - and must be relied upon to initiate any action that resets the reference. In many cases this is not worth the effort of solving (an obsolete reference will only hold memory for a single forgotten object) unless the referenced object also holds non-memory resources that must be released in a timely manner. (A subject we'll revisit later.)
It becomes more important to deal with problems when there is a possibility of holding references to multiple objects. For example, it is possible to implement a stack using an array and a "top" index. If references to multiple objects are pushed onto such a stack the objects will be held in memory until the entries in the array are cleared (or the array itself becomes eligible for garbage collection). This implies that the "pop" operation should reset the reference to the old "top" element.
Collections are where the problem becomes severe: for example, there is a long-lived collection buried in the depths of the Swing library that maps events to listeners. User code that adds listeners to Swing objects causes these objects to be added to the collection. In practice user code to remove these objects is rather rare and, in early versions of Swing, this used to lead to the collection becoming progressively larger each time a dialog (for instance) was displayed. More recent versions of Swing addressed this problem by using a special type of container: one that holds weak references.
Weak references are a feature introduced with JDK 1.2 with WeakHashMap and WeakReference. These allow some additional flexibility in garbage collection. A weak reference is recognised by the garbage collector as one that does not prevent an object being collected (but that must be reset should the object be collected). They allow the developer to keep track of an object for as long as there is a use of it somewhere else in the program, but to release it once that use is complete.
The use of WeakHashMap in the Swing library is typical of the scenarios where weak references are useful: in the OBSERVER pattern the subject should rarely affect the lifetime of the observers.
In addition to the need to put objects away, there are also objects that need to be switched off. It took a lot of batteries going flat before my children learnt to switch off torches, walkie-talkies, Gameboys and other toys that contain batteries. Java objects can also contain resources that need to be "switched off" - graphic contexts, file handles, etc. These are objects that need to know when the programmer has done with them. For these objects the programmer needs to develop the discipline needed to supply the required notification.
In the vast majority of cases this is simply a matter of putting a call to a release method where it will be executed on exit from a block of code. There is even a convenient language construct for doing this: the finally block. It looks like this:
public void repaint() { Graphics g = getGraphics(); // Allocate if (null != g) { try { paint(g); } finally { g.dispose(); } // Release } }
In this code the scope rules are used to ensure that the paired operations of allocation and deletion always occur as a pair (and in sequence). This isn't hard - although correct examples are rare in the literature.
As with managing memory the issue becomes problematic when references to the owner of the resource are long lived (i.e. have instance or class scope). If the graphics object in the above example were referenced by a class member and accessed by a number of methods then it could be difficult to determine when use was completed. It may become necessary for the owning object to provide its own equivalent of the dispose method and to rely on its user to call it.
The management of resources is at its worst when there are longlived references to the same resource-owning object in independent parts of the system. If this happens it can be very difficult to release the resource at the right time - without either implementing some convention for communicating between them or electing one to be "the boss" none of these can confidently release the resource. Fortunately, in real code this is rare. (There are options: weak references, proxy objects that hold a use count, etc.)
I hope this has shown that there are memory management issues to be addressed in Java and that these issues can be addressed. As so often in our profession it is the acknowledgement that there is a problem that is the key step to finding a solution.
[1] Although C++ objects may also be created dynamically like Java objects (that is, by using new) these are idiomatically managed by special "smart pointer" classes that ensure that the memory doesn't leak. But, just as there are many Java books that fail to teach idiomatic memory management, there are many C++ books that fail to teach this.
Overload Journal #59 - Feb 2004 + Design of applications and programs
Browse in : |
All
> Journals
> Overload
> 59
(7)
All > Topics > Design (236) Any of these categories - All of these categories |