Taking out the trash
Make sure you understand Java garbage collection before you design your next Web application
In this first of two columns, Navneet Mathur provides a technical explanation of the client-side problems he unearthed while working on a large-scale Java project. If you want to really understand Java's Garbage Collector, you should read this column. Next month, Navneet will cover Java virtual machine (JVM) performance issues. (3,000 words)
Over the next two months, IT Architect will cover design issues for Java-based application architectures. The systems integrator I work for had many successful Java applications last year, but there was one application that ran into difficulty. As a result of this particular project, everyone associated with the application learned once again that there is a lot more to designing a great application architecture than being able to turn business logic into code.
Architects need to consider the strengths and weaknesses of any technology they plan to use in an application architecture. In this case, it was the JVM. I'm sure everyone reading this set of articles has heard comments about the JVM's performance, memory management, and code compatibility. However, I find that the commentators are rarely able to articulate exactly what it is that causes their concerns -- and more importantly, how best to go about doing application design given the current state of the JVMs.
really like working with Java. To be perfectly frank, I'm writing this column to help make Java a more popular development language. I'm hoping that by covering some of the issues that are preventing Java from attaining broad consumption, I'll make things easier for developers -- and perhaps spur Sun to resolve these problems in the next release of the JVM.
Java provides the ability to create graphical user interface (GUI)-based applications as good as industry-standard development tools like Microsoft Visual Basic. We would ideally like to use Java to build dynamic screens (screens with content that changes depending on the user profile) for Web-based applications that mirror the look and feel we're accustomed to on non-Web-centric applications.
Much has been written on the superiority of Java over HTML for client-side Web applications. There have been some articles on issues with Java applets on the client side, too. But most of this writing seems to be based on small pilot applications developed in a lab.
This column is based on lessons learned on a 70 man-month project with about 100 screens. The application we created has been used by 50 concurrent users for the last 3 months. This will increase to 350 users by next year. Deploying an application into a production environment brings up issues that might not be apparent in a smaller project. We deployed the application on Windows NT 4.0 (JDK releases 1.1 through 1.1.6), but the issues discussed here are at the design level and are equally valid on Unix.
Why choose Java on the client side?
We wanted a platform-neutral environment for our application, so the choice essentially came down to either Java or an HTML-based solution.
The network on which we were to deploy our application featured fairly powerful Windows machines, and we wanted to take advantage of this. Choosing Java let us run our Web-based application on the client side.
Other reasons you might choose Java over HTML are:
With Java, the screens can be made very flexible, and have built-in validation and business rules that kick in as the user enters data. For example, if two fields on a screen are related, data entry in one field can enable the second field. Using HTML, this kind of data entry would require two stages. Java also gives you the look and feel of a standard GUI application using multiple overlapping screens and pop-up modal windows. Instead of passing an HTTP string for messaging between the client and server, Java lets you use more robust object-messaging schemes like industry-standard Object Request Broker (ORB) middleware.
Another advantage of using Java on both the client and server sides is that it reduces the number of programming languages and technologies needed for your project.
Issues with client-side Java
At this point in Java's colorful life, there are still many problems involved in using it on the client side. Don't get me wrong -- I'm not saying that Java is dead on the client. I'm merely pointing out some of the problems it's suffering from today -- issues you'll need to understand before you embark on any Web-based Java application development. Whether or not Java is a prudent choice depends on the size and nature of your application. In some cases, you may need to do a workaround or two, but you'll find that the job is at least possible. In other cases, you may want to stay away from client-side Java until several important issues are resolved.
Java garbage collection
Java's Garbage Collector (GC) has three major goals:
Automatic garbage collection was greatly hyped when Java was first introduced to the masses. No more delete statements and fewer memory leaks, they said: Java takes care of it all by the automagic of its Garbage Collector. The promise is still there, and I believe it will become a reality in the near future, but as of today there are a few fundamental problems within Java's garbage collection scheme. The Garbage Collector does work automagically, but not when you want it or how you want it. These problems are especially visible on large online transaction processing (OLTP) applications.
Trying to control the 'when'
Several interrelated "when" issues form the single biggest roadblock to Java's acceptance on the client side for large mission-critical applications. OLTP applications typically have a large number of screens that are used in quick succession when objects are collected -- as was the case with the application I was working on. For these types of applications, it's critical that most of the objects associated with the screen are garbage-collected as soon as the screen is closed by the user. This lets you reuse memory heap space when your user instantiates a new screen. If the GC doesn't collect these objects in a timely fashion, the heap runs out of memory and requests the OS to allocate more memory to the JVM. Ultimately, this causes the system memory to be unavailable for other applications.
The problem with the GC is that you don't have the ability to force a garbage collection -- you can't force a keyword garbage collect. You know you have a memory shortage when you get OutOfMemory exceptions (on the Microsoft VM this is a ComFail exception) at random points while running the application. This problem is especially apparent if you open screens, enter data and close them in rapid succession. What happens is that the GC is unable to kick in before new screens are opened, since the rate of garbage collection is far slower than the rate at which new screens are opened and new objects created.
In the project I worked on, this was a perplexing problem to diagnose
due to its completely random nature. We started looking in the obvious places. We looked
closely at our code and streamlined it, then we looked at increasing the
memory allocated to the JVM by using the -ms and -mx startup parameters. We even
used JVM-monitoring software like OptimizeIt, but got nowhere. Through this
process we developed a strong suspicion that the JVM was
not garbage collecting efficiently, so we decided to build our own Object
Watcher class. We had the objects register with this class at
instantiation and unregister, using the
finalize method, as
they were garbage collected. The finalize method is called at the time
the object is garbage collected and the object unregisters itself with the
object watcher. (You can
click here to
see our Object Watcher class code. If you're interested in reading more
about this, I recommend Sun's documentation on
Writing a finalize method.) We created a floating window with a Debug button. This
debug was available at all times, and hitting it produced a
dump of the number of objects instantiated for each class from the Object
Watcher. This, along with a print statement in the unregistration code,
helped us track when objects were being garbage collected.
I highly recommend that you use the Object Watcher class as part of your application framework design. It can pay rich dividends during development and testing and save you a lot of debugging time.
At this point, you're probably thinking, "That's great. They found the problem -- now how do I solve it?" This is where two other garbage collection issues come into play.
Trying to control the 'how'
The Java GC runs as a separate low-priority thread, either synchronously or asynchronously depending on the situation and the system on which Java is running. It runs synchronously when the system runs out of memory, or in response to a request from a Java program. It runs asynchronously when the system is idle, but it does so only on systems (such as Windows 95 and NT) that allow the Java runtime environment to note when a thread has begun and to interrupt another thread. As soon as another thread becomes active, the garbage collector is asked to get to a consistent state and terminate.
In other words, the GC is an autonomous program that runs either synchronously or asynchronously. There is a provision to run the GC under program control by using the System.gc() call, but in practice this doesn't run the GC synchronously, because the low-priority thread doesn't get a chance to execute immediately. We tried using the -noasyncgc JVM start-up parameter so that the GC would only run synchronously, but this didn't help either.
The inability to control when garbage collection happens adds to the problem discussed earlier. In some cases it's more important to collect the garbage objects than to continue execution of the main thread of the application -- especially when a memory threshold is reached, or before opening a large screen, or after closing a large screen. If it were possible to control the GC from within the application -- that is, by bumping up its thread priority or ensuring garbage collection was complete before resuming execution of threads within the app -- the stability of the application would be increased. The GC runs as a low-priority thread so as to maximize application performance, which results in a trade-off between performance and stability. For mission-critical OLTP applications, most people would sacrifice a little performance for a little more stability, especially since users tend to be frustrated by system crashes.
In our application, we found that if we were able to run the GC synchronously after some of the larger screens were closed, we would be able to recycle a large amount of memory, thus eliminating the OutOfMemory exceptions that had plagued us.
Identification of garbage objects
The Java garbage collector is a mark-sweep garbage collector. A mark-sweep garbage collector scans dynamic memory areas for objects and marks those that are referenced. After all possible paths to objects are investigated, unmarked objects (that is, those that are unreferenced) are known to be garbage. Theoretically, they are then collected during the sweep run. However, we occasionally found that unreferenced objects weren't collected during the sweep. I think this happens because the GC requires additional runs to clear nested objects (that is, objects containing other objects) from memory.
The solution to this problem is quite straightforward. We found that explicitly setting the object references to null within our code helped the GC identify garbage objects. This is counterintuitive, though -- it throws us back to the days of C++ and delete() statements. This, again, raises the question: Is it prudent for the GC to be running as an asynchronous low-priority process?
Take a look at the Java bug-parade bug id 4102107 on Sun's Web site and send in your vote.
Garbage collection: Some solutions
As you can see, it's impossible to predict when objects are to be garbage collected, which ones will be garbage collected, and how much memory will be freed up by this action. To make matters worse, the Java API does not allow you to query the amount of free memory available to the JVM and the OS. If this were possible, one could wait for the GC to free up memory before instantiating a new screen. As it stands now, OutOfMemory exceptions caused by garbage collection issues result in unstable applications. Such instability is definitely not suited for large-scale OLTP applications that serve large numbers of users, like extranet e-commerce applications. However, on other internal strategic applications with just a few users, Java on the client side can work. Large applications of a more processing-intensive nature, with few front-end screens, are also a good choice for a client/server Java architecture.
During our investigation of garbage collection issues, we also found that some of our third-party screen components (widgets) occupied a large memory footprint. Again, after digging into it for a while we found that this was due to multiple images (image objects) being created for every control on the screen. This is a technique known as double buffering, and it's commonly used in the graphics industry for flicker-free drawing. Using this technique, pre-constructed off-screen images are used to rapidly change the appearance of a screen component. These additional pre-constructed images occupy extra memory.
To solve this, we delved into our third-party code and turned off the double-buffering mode. This drastically cut down the memory usage and virtually eliminated the problems we were having with OutOfMemory exceptions. As for the flickering, there was no appreciable difference except where animated GIF files were in place. As a test, we asked a few users to use the application after we had removed double buffering. They found the application to be very stable and were able to use it continuously for four hours (after this, we stopped the test) without noticing any flickering on the screen.
Based on this experience, I suggest that you evaluate your third-party screen components or the components that come with the Integrated Development Environment (IDE) you plan on using. Most of the screen component vendors use the double-buffering technique, but not all of them have the ability to turn off double buffering in a straightforward fashion. It's critical that you complete this evaluation during the technical-design phase. (Take a look at the Java bug-parade bug id 4088950 and bug id 4014323 on the Sun Web site for more on this).
It's also imperative that developers explicitly set object references to null for optimal performance and stability. In fact, I strongly urge companies to make this practice a part of their Java development standards.
Other workarounds that can alleviate garbage collection problems include more planning on the client machines during the design phase, and training your users on how best to use the application. In particular, it's useful to train users to report some of the issues that arise and work around them. This training decreases the frustration level of the users and helps with the assimilation of the application. Unfortunately, these workarounds cannot be employed on extranet applications, where you have almost no control of users and their machines.
I recommend Windows NT 4.0 with 64 megabytes of memory for PC clients. We initially planned to roll out our application on the Windows 95 platform, but during testing we found that the application stability drastically increased on Windows NT. This is because Windows NT has better memory management than Windows 95.
I hope this article brings Java's garbage collection issues to the forefront, and that these issues will be addressed in the upcoming JDK 1.2 release. It is critical that the Garbage Collector is made configurable and flexible if client-side Java is to be ready to handle mission-critical OLTP applications. This is a problem for server-side Java as well, but less so since more memory can be easily added and object turnover is less frequent.
Next month I'll cover Sun's and Microsoft's JVM compatibility and performance as they relate to client-side Java. I'll also provide recommendations on how to extract better performance from Java in general.
About the author
Navneet Mathur is a senior associate and technical team lead at Cambridge Technology Partners. Reach Navneet at email@example.com.
If you have technical problems with this magazine, contact firstname.lastname@example.org