With the creation of what is referred to as "Internet-time," the time-to-market (or development time) of an application has become very important. A one year development cycle could lose a company's established market share. Even a six month development cycle could be detrimental to a company's continued success. As a result, it is possible that during a rapid application development cycle, "best practices" are ignored. Due to time constraints, developers may create code without weighing the outcome of the implementation they choose. In many cases, this translates into buggy code. It also translates into a more costly development cycle as the maintenance phase requires more effort and lasts longer. Unfortunately, rapid application development can also translate into a lower solution performance than what should have been obtained.
There could be many reasons why performance, or lack thereof, is a direct result of this "Internet-time" development cycle. In some cases, the developers working on the solution are inexperienced with the problem domain. In other cases, the developers are unfamiliar or inexperienced with the tools they are using. The latter could be more detrimental than the former. Despite the technical recruiter's attempts of equality, Microsoft's Visual Basic® and Java are two different programming languages. Likewise, C++ and Java are two different programming languages. Therefore, someone experienced writing solutions using only Visual Basic is more likely to create a Java solution that performs slower than a developer who has been working with the Java language for four years. This does not mean that the Visual Basic-made Java developer is less of a programmer than the Java developer. Nor does it mean that the Java developer is a better programmer than the Visual Basic developer. It could just mean that the Java developer understands and has more experience with the tool he/she is using to create a solution.
One must realize that even the most experienced Java developers are not perfect. In fact, if a developer learns poor habits early, and continues to follow those habits, the resulting solution could be worse than that of the Visual Basic-made Java developer. Automatic memory allocation and deallocation, one of the many advantages of the Java language, could also be one of its biggest disadvantages. Having a language with automatic memory allocation and deallocation allows developers to become lazy. Unfortunately, this laziness can cause poor performance. Likewise, dynamic class loading, though it provides an extreme amount of flexibility, can also be a disadvantage. The design and implementation of a class can adversely affect the execution performance (as you will see below). The Java language provides many features which lessen the development effort required to create a solution. However, many of these same features, due to their automatic behaviors, can adversely affect the performance of the solution. If developers abuse these features, intentionally or unintentionally, the performance of the solution can be affected. Therefore, it is extremely important to be conscientious of the implementation you choose.
Hopefully by reading this article, you will learn (or re-learn) some important tricks that you can incorporate into your solution to enhance performance. These tricks alone will not deliver the promise of native application execution speeds. They may improve performance enough to brighten your end-user's experience. It is important to realize that performance of your application is a direct result of many different factors, from processor speed to the amount of random access memory you have. When considering performance in the context of a Java solution, it is imperative to factor in the performance of the virtual machine itself. You will find that different virtual machines (including major and minor revisions and vendors) have different performance. Therefore, I suggest analyzing the performance of your virtual machine as part of your overall performance evaluation. At the end of the article, you will find a list of resources where you can obtain Virtual Machine performance data.
This article also presents both coding tricks and design tips to better performance. It is important to realize that whenever you embark on an optimization journey of an existing solution you should use a profiling tool. I highly suggest fixing the areas of question outlined by a tool prior to blanketed, hack-like attempts to optimize your code. Also, it is worthwhile noting that writing a solution for good performance may have tradeoffs, from design to maintenance. It is up to you as a developer to discern which tradeoffs are more important. This article does not try to teach you how to take an existing application and make it perform better. Rather, it presents some "things to think about" while developing a Java solution.
Execution speed can be a direct result of the implementation you choose to provide some desired functionality. In some cases, the implementation you choose is the best possible implementation in terms of performance. In other cases, a "lazy" implementation may yield poor performance. A common design for a class that contains multiple constructors is to have a constructor use another constructor, which uses another constructor to provide the initialization. Though this makes development and maintenance easier, it may hurt performance. For example, take the code listing below. The listing contains three different ways to implement a class called SlowFlow, one of which could yield slower execution than the other two. In all three cases, SlowFlow has three constructors, which initialize the object. However, the execution flow of the initialization does not follow the same path. For example, the execution flow of the first column has more overhead than the execution of the third column, as the first column relies on two additional method invocations. The second column is better than the first as we have reduced the number of method invocations required for the object initialization.
| Listing 1: Object initialization constructor design | |||||
| Column 1 | Column 2 | Column 3 | |||
|
|
| |||
As you can see, the result of each implementation is the same, someX and someY are initialized with the values of x and y passed into the third constructor. However, the paths taken to reach the initialization are different. In the first column, calling SlowFlow() to create a SlowFlow object, calls SlowFlow(int x), which in turn calls SlowFlow(int x, int y). SlowFlow()'s execution does not return until SlowFlow(int x)'s execution returns which can't return until SlowFlow(int x, int y) returns. This flow could adversely affect the amount of time required to create the object. The second column presents a better solution, however, as it relies on another method for the initialization. The last implementation of SlowFlow (3rd column) performs the actual object initialization in each constructor, or in a single method call. This may result in faster object creation speeds. Realize there is a tradeoff involved; the maintenance of the code in the third column may be more intensive as the maintenance may need to occur in every constructor. This type of design, or optimization technique, is normally referred to as code in-lining or method in-lining. Code in-lining is a useful technique as it saves execution overhead by lowering the number of method calls on the execution stack. Code in-lining is useful in both constructor and method implementations. Some Java compilers will perform this functionality for you and some may not. Others may perform code in-lining of methods but not constructors. Therefore, it is important to know what optimization functionality your compiler provides.
Another important aspect of performance deals with the amount of time required to "install" a class into a virtual machine. The Java language utilizes a dynamic, run-time binding enabling solutions to dynamically install classes, as needed, over time. This type of model is probably most common in the Java applet solution scenario. Though network class loading is the most apparent in applet solutions, it is also utilized in Remote Method Invocation (RMI) and Jini solutions. It can also be used in application as well as enterprise solutions like servlets and Enterprise JavaBeans. In the applet architecture, the classes used to run the applet are normally loaded from a remote Web server. Remember all of the "system" classes are loaded from the local JVM library, but the implementation of the applet class and its associated classes are loaded via the network. Therefore, the number of resources and the size of the resources that have to be downloaded over the network can effect the performance of the applet.
A common solution to decrease the amount of time required to download the appropriate resources is to use a Java Archive (JAR). A JAR is a collection of resources compressed into a single file. Using a JAR allows the recipient to make a single socket connection and transfer a single file to retrieve all of the resources. Without the use of a JAR, the resources are normally retrieved on a one-by-one basis, possibly reusing the same socket connection. As transfer speeds become less and less of an issue, the amount of time required to load a class, or a set of classes, from the network will lessen. However, until everyone has a high-speed connection, download times should be considered. Creating an archive that contains tons of data in a compressed form definitely lessens the latency. However, relying on JAR to compress your classes should not be a reason to create classes that don't compile into the smallest possible footprint. The smaller the class, the less time required to load it into memory. Code in-lining not only enhances execution speeds, but also reduces the compiled size of a class. Using the listing above, the compiled versions of each implementation go from biggest (on the left) to smallest (on the right). In the context of these three implementations, the difference is a couple hundred bytes, which may not seem like a lot. However, if your application loads 10 classes that utilize code in-lining, you could save a few kilobytes. If it loads 100 classes, you could save a megabyte. Class size is an important thing to remember when dealing with optimization and performance, especially if you are using network class loading.
On that note, another way to minimize class size is to extend an existing class rather than implement an interface. This may not always be possible nor desirable, but as with the in-lining above, you have to weigh the tradeoffs. Extending a class allows you to override the methods of your desire. Implementing an interface requires you to implement all of the methods, even if you are only interested in one or two. If there is an "abstract" class that provides a default implementation for all of the methods in an interface, you may find that it is better to extend that "abstract" class and override the desired behaviors.
The code listing below provides two classes that handle window closings. The listing on top extends the WindowAdapter class as the "abstract" class. The listing on the bottom implements the WindowListener interface. If you look at both implementations, they provide the same functionality; they handle WindowEvents . In particular, they handle WindowEvents of a window that is closing.
| Listing 2: "extends" versus "implements" | |
| |
|
MyWindowListener , despite its desired behavior to only implement windowClosing(WindowEvent) , provides an implementation for every single method declared in the WindowListener interface. Even though the implementations of these methods are empty, they are contained in the compiled version of this class (otherwise the class would not be a WindowListener ). This places some overhead on the class size, whereas MyWindowAdapter only contains an implementation of windowClosing(WindowEvent) . Since the other WindowListener methods are inherited, they are not included in the compiled version of class. This allows the class size of MyWindowAdapter to be smaller than MyWindowListener . In fact, MyWindowAdapter has a class size of 476 bytes; whereas MyWindowListener has a class size of 843 bytes. I will admit we are only talking 400 bytes here, but remember the smaller the class, the less time to load! If there were 10 classes in a given solution that had the opportunity to inherit instead of implement, you could save a couple kilobytes. In terms of network class loading, this could save your end user a second or two of time.
There are other cases beyond event listeners where extending may make more sense than implementing or even creating your own implementation. For example, if you want to create a class that provides linked list functionality, from a class size perspective, it may be better for you to extend java.util.LinkedList instead of writing your own from scratch. Likewise, if you want to change the way a JButton is drawn, you might find that it is better to extend an existing ButtonUI class (like javax.swing.plaf.metal.MetalButtonUI ) than create your own. There are two direct results of extending a class versus implementing the interface. One result is the smaller class size (which as noted above could lower the amount of time required to load the class). The other result is the fact that if you extend a "system" class, the inherited functionality will be loaded locally, again saving time.
Now that we have discussed some design tips, let's look at some tricks you can use to improve the performance of your solution.
Just about every Java solution uses streams to read and write data. The process of reading and writing data can be extremely costly in terms of performance. Since the I/O is normally provided through the use of some native library layered beneath Java programming, there is some default overhead incurred. However, the type of stream you choose can also affect performance. The Java language provides two types of streams: Readers and Writers and Input and Output streams. Readers and Writers are great for reading and writing data at a higher, more abstracted level. For example, Readers and Writers are great for reading and writing String data. Input and Output streams provide data access mechanisms at a much lower level, the byte level. In either case, if your solution demands fast consecutive reads and/or writes, you might consider using the buffered stream permutation. Buffered streams make use of an internal data buffer. This allows I/O operations, such as reading data, to perform better. In the context of reading, upon the first read, the internal buffer is "filled" with data. Each consecutive read is read from the buffer until the buffer is empty, at which point the read will cause the buffer to fill again. Realize there is a memory implication when you use buffered streams. The default buffer size for both input and output is 2048 bytes. You can adjust the size of the buffer by calling the appropriate constructor. It is important to realize that the buffer is held in RAM. Therefore, a 30 kilobyte buffer may not be desirable as it limits the amount of RAM the JVM can use.
Object comparison can be another costly operation. Whenever possible, try to compare objects based on the reference value instead of their actual value. In the context of Java code, try to compare objects using = = instead of equals() . Though this is not always possible, the object evaluation using = = is faster as two integers are equated. Every Java class has the ability to define its own equals() method. If you want to compare your objects using equals() , provide an implementation of equals() in the class. However, providing your own equals() method does not guarantee better performance over the inherited or default equals() method. If you do provide an equals() method, try to avoid comparing member variables based on their String representation. It sounds silly, but I have seen it quite a few times. A String comparison (using equals() ) checks to see if the reference value of each String is the same. Assuming they are different, a character-by-character comparison is used to compute equality. Taking the two words "kelby zorgdrager" and "kelby zorgdragor", this character-by-character evaluation could be costly as the first 14 characters are the same. Not until the comparison reached the 15 character, would it "realize" the two Strings were dissimilar. When writing your own equals() method, consider using this flow:
- Check to see if the object references are the same.
- If they are not the same, check the internal member's values.
One of greatest advantages Java programming has (over C++) is its automatic memory allocation and deallocation provided through the garbage collector. A common tendency (and pitfall) as a Java developer is to abuse the garbage collector and the functionality it provides. Just because you, as a developer, don't have to malloc and free memory, does not mean this operation doesn't occur. It does occur; but it occurs behind the scenes. Allocating and deallocating memory can be expensive in terms of execution speed and system resources. Haphazardly creating temporary objects on any platform can seriously affect the performance of an application. Reaching the "free" operation in Java programming could be time consuming, as the garbage collector needs to determine if it can collect a given reference and its associated space. In fact, the garbage collector runs as a background thread and "runs" only when there is a free cycle. This means that your application could be consuming more memory than needed while it waits for the garbage collector to run. Therefore, make every attempt to be conscious of when memory is being allocated and when it goes out of scope. You may even want to consider "nulling" out objects when you are done using them. Not only does this make your code more explicit, it also "helps" the garbage collector as the reference validity determination process is lessened. Since there are different garbage collection algorithms found in different implementations of JVMs, you many want to explore the garbage collection performance of different VMs.
There are a couple of other tips and tricks you should consider when developing Java solutions:
- Use collections wisely.
In many cases, collections are represented as arrays behind the scenes. For example, if you add 30 objects to a collection and then remove 25 of the 30 objects, your collection will minimally be an array 30 elements long. If you are not going to use the other 25 "spaces" in the collection, you may want to consider trimming it. java.util.Vector has a trimToSize() method that does just that. Trimming can be detrimental to performance if the collection frequently grows in size. You may also want to consider setting the "growth" factor of the collection you are using. Vector's growth factor doubles it size when it runs out of space; the exponential growth could definitely cause you to lose free memory very quickly. - Reuse objects.
A solution may require you to create 20 instances of a Foo over its execution lifetime. Creation of objects can be a costly operation (in terms of memory allocation and constructor execution). Therefore, you should consider reusing objects as much as possible. There are object-oriented design patterns that address object reuse without having to create a new object. A common strategy is to have an init() method that performs the initialization of an object. This allows you to create the object, use it, reinitialize it, and then reuse it. - Reuse threads.
As noted above, it is costly to create a new object. Creating a new thread object is even more costly than creating a new object (as there are system resources tied to a thread). Therefore, try to reuse threads as much as possible. Commonly thread reuse is obtained through the use of a thread pool in your solution. - Be kind when redrawing.
Painting and repainting a user interface in Java programming can be a slow operation, especially if the entire graphics area is redrawn. Therefore, when performing intensive drawing operations, consider repainting only the "dirty" area. You may also want to consider using a backing-store (off-screen buffer) to set up your drawing and then paint the backing-store's image.
Despite the many advantages of the Java language, there are some inherent disadvantages (though many of the disadvantages are advantages that have been abused). The disadvantages are quite evident when the performance of a Java solution is examined. There will always be areas where developers can improve their implementation to improve the performance of a solution. Hopefully, this article provided some insight on where and how to get started. I challenge you to be mindful of the implications your design and implementation have on performance and memory usage. Remember to always integrate the use of a profiling tool in your attempts to increase performance and lower your memory footprint.
-
Virtual Machine performance test numbers
-
Optimizeit tools
-
Performance tools
-
Java Virtual Machine Profiler Interface (JVMPI)
-
Further description of JVMPI
Kelby Zorgdrager is a Sr. Software Engineer at Sun Microsystems Inc. Prior to working as a Software Engineer, Kelby was a Sr. Java Instructor for Sun Educational Services where he provided training and consulting services to over 2500 students. Kelby has presented at major Java trade shows including JavaOne, Java Business Expo, and COMDEX. Kelby has worked with, and developed in, the Java language since January of 1996.
