Wilfred Jamison

Subscribe to Wilfred Jamison: eMailAlertsEmail Alerts
Get Wilfred Jamison: homepageHomepage mobileMobile rssRSS facebookFacebook twitterTwitter linkedinLinkedIn


Related Topics: Java EE Journal, Java Developer Magazine

J2EE Journal: Article

Improving Performance of J2EE Applications

Improving Performance of J2EE Applications

In the Java programming model, one of the best-known performance issues is what we refer to as the garbage collection bottleneck. Garbage collection (GC) reduces the degree of concurrency in the JVM, where at worst only the garbage collector thread executes while all other threads are suspended, thus producing no work for the application until the collection is over. Consequently, the longer the GC takes, the longer the threads are suspended and therefore the less throughput and the slower response time will be. Garbage collection also introduces scalability problems in multithreaded applications and SMP systems.

Advances in garbage collection research focus on increasing the degree of concurrency during garbage collection. Some of them, such as parallel collectors, concurrent garbage collection, and incremental garbage collection, are already introduced in the newer JVMs. Despite these new approaches, garbage collection remains a fundamental challenge.

The focus of this article is to give the readers a closer look at the cost of memory usage and how it affects performance. Then I present some ways in which developers and software engineers can improve performance of J2EE applications by reducing the cost of garbage collection.

How Expensive Is an Object?
The cost of an object is directly attributed to the amount of memory overhead and the management cost associated with it. One of the primary reasons why performance can be a problem is the way memory is allocated and collected dynamically in Java. All nonstatic objects, which comprise the majority of objects in a Java application, are created from the runtime heap. The heap itself is protected whenever memory is allocated or collected. Furthermore, the JVM specifications provide semantics in which threads have a local copy of memory used in the heap. The copies then have to be synchronized with the master copies. Thus, as more and more memory is used and collected, more overhead is incurred along the way.

The most fundamental cost of an object is incurred during its creation, which is dependent on the following:

  • Class size: The more methods and attributes are defined, and the more complex the methods are, the bigger the resulting bytecode will be.
  • Class structure: The deeper the inheritance relationships are, and the more object compositions there are, the more complicated the class structure will be.
  • Class paths: The more entries there are in the classpath, the longer the search time will be.
  • Available memory: The less memory that is available, the more time is spent on managing and allocating the amount of memory required.

    These costs are distributed in different stages of object creation.

  • Class loading: If the class of the object being created has not been loaded in memory yet or if it has been garbage-collected, then class loading is included in the object creation procedures. The cost includes traversing through the classpath and searching through an archive or directory listing. Thus, a considerable file system search overhead is involved. It is wise to place the paths of the most commonly used classes early in the classpath. The actual class loading is performed in a synchronized fashion. The cost of class loading is a function of the size of the class. Furthermore, if the class is a subclass or if it implements an interface, then the superclasses or interfaces will have to be loaded as well, if they are not loaded already. Classes and interfaces are loaded into the method area of the JVM. If not enough space is available to load a class or interface, garbage collection is triggered.
  • Object memory allocation and initialization: The next stage is the actual creation of the object whereby memory is allocated for the object. Typically, memory for the attributes of the object must be allocated, including some system-level storage, e.g., monitor, waiters list, etc. If there is not enough memory, garbage collection starts. Note that the total memory required by the object would be the sum of the memory requirements of each of the superclasses and interfaces in its inheritance tree. Any initialization of attributes is also performed. Note also that the initialization may involve creating new objects, in which case another round of object creation is performed.
  • Constructor chaining: At this stage, the constructors of the object are executed. Each and every constructor in the hierarchy chain is executed, from the topmost down. Note also that within the constructor, new objects may be created.

    The Cost of Garbage Collection
    Note that object creation is the primary reason why garbage collections occur. Garbage collection can take place in any of the stages of object creation. It can also occur multiple times in a single object creation event. The performance of garbage collection varies depending on the current size of the heap and the constraints specified, such as the minimum percentage of available memory, etc.. The performance cost is composed of the following:

  • Marking time: The length of time the garbage collector spends traversing through all of the reachable objects, starting from the root set and marking each object as being "used."
  • Sweeping time: The length of time the garbage collector spends traversing through the entire heap to determine which objects can be reclaimed by checking for the "used" mark; it also includes the time spent in executing finalizer methods of objects to be reclaimed.
  • Compaction time: The length of time the garbage collection spends in moving all used memory to one side so that available memory is contiguous; it also includes updating all references to objects that were moved.
  • Expansion/Shrinkage time: The length of time the garbage collection spends in expanding or shrinking the heap depending on the conditions met.
  • Copying objects: For garbage collections based on generational algorithms, objects are copied back and forth in the young generation space and their ages are monitored for tenuring.
  • Execution of finalizers: The management of objects with finalizers and their actual execution are costs that should be considered seriously as well.

    Given this, a smaller heap may be faster to collect. However, if the application is aggressive with creating objects, garbage collection can occur more frequently. Bigger heaps have the advantage of fewer garbage collections, however, a garbage collection can take longer. Searching for available memory may also take longer depending on how memory management is implemented. In severe cases, frequent paging may also occur.

    Since garbage collection prevents application threads from doing useful work, there are two main concerns: (1) the duration of a garbage collection and (2) the frequency of garbage collection. The duration of a garbage collection depends primarily on the size of the heap and the amount of live memory (memory that is currently being referenced and used by an application), which we will refer to as the memory footprint or working set of the application. The frequency of garbage collection is also influenced by the size of the heap and the rate at which the application creates objects, which may also be dependent on the number of active worker threads. Thus, the goal is to reduce the overhead of garbage collection by keeping its duration as short as possible and its frequency rate very low.

    The Heap Size Challenge
    Choosing the values for ms (initial heap size) and mx (maximum heap size) is one of the major challenges, and it gets harder as your application becomes more complex. As we have already noted above, the heap size is critical to both the duration and frequency of garbage collection. The goal is to find a middle ground in which we can balance both duration and frequency.

    Here are some simple guidelines for setting your heap size.

  • Set your performance requirements: Quantify your expected throughput and response time.
  • Estimate your mx and ms based on your knowledge of the application's memory usage pattern.
  • Stress test your application with the expected average workload.
  • If the results do not meet your performance requirements, analyze the application's garbage collection statistics. Use the -verbosegc output from the JVM to examine how long your garbage collections take and how often they occur.
  • If the duration/frequency of garbage collections is unacceptable, fix the problem by adjusting your heap settings.

    Typically, a garbage collection that takes more than a second on the average is indicative of either a high memory footprint or too big a heap. Also, an acceptable rate of GC occurrence is between 3% and 5% of the time.

    Most JVMs also offer a way to control the heap size dynamically by specifying minimum and maximum percentages of the heap that must be kept free, as well as the minimum and maximum amount of memory that can be added to or removed from the heap.

    What Application Developers Can Do
    Setting and controlling heap sizes are things that can be done outside of the application itself. Experience shows, however, that the majority of the performance improvements can be achieved by improving the way the application is written. In the extreme case, this may involve redesigning the architecture of the application. The less extreme case involves finding better ways to implement certain parts of the application. In this section, we shall assume the latter, that is, a careful analysis of the design has been made and that it is acceptable. Below are some best practices that I refer to as maxims from which developers can learn to minimize the use of memory. The goal of these maxims is to minimize live memory at any point in time.

    Maxim 1: Simplify Your Class Structure
    Memory usage is directly affected by the complexity of the class structures in the application. Not only do complicated class structures consume more memory, but they also take longer to get loaded.

    Simplicity of class structures also makes understanding program logic much easier, and thus makes debugging easier. Take note also that interfaces are cheaper than using superclasses or abstract classes. Some things to remember when designing a class are:

  • Keep your constructors short and simple.
  • Avoid too much specialization.
  • Prefer composition to inheritance.

    Maxim 2: Do Not Create Objects Prematurely
    The idea is to avoid the cost of creating an object that may be left unused anyway. This is possible if later in the computation some conditions must be met in order to use the created object. The following snippets provide a very simple illustration. The method stores a String object in a Vector only if the length of the String is between 1 and 32. In the case of longer Strings (or the rare case of a 0-length String) the Vector object is created but never used.

    Premature creation can also happen when a class field attribute has been initialized, either in a constructor or during object initialization, but then either the field does not get used at all or it is used much later. By creating objects prematurely, the chance of garbage collection occurring sooner becomes higher.

    private Vector foo (String obj)
    {
    Vector v = new Vector ();

    if ( obj != null && obj.length() > 0 &&
    obj.length() <= 32) {
    v.addElement(obj)
    return v;
    }
    else
    return null;

    }

    The recommendation is to always move object creation as close as possible to the location where the reference to the object is going to be used. We can rewrite the foregoing example as:

    private Vector foo (String obj)
    {
    if ( obj != null && obj.length() > 0 &&
    obj.length() <= 32) {
    Vector v = new Vector();
    v.addElement(obj)
    return v;
    }
    else
    return null;

    }

    Also, you can follow a lazy initialization approach wherein a null reference is checked at the point where the object is actually needed. If the reference is null, then the object is created. Checking for a null reference is a very fast operation.

    Premature creation may not be as rampant during the early part of development. Most programmers know the right place to create objects. However, it is when the code is updated and maintained that these things become unnoticeable or aren't checked.

    Maxim 3: Be Careful With Hidden Objects
    This maxim points out the fact that some methods create temporary objects that we may not be aware of. Some of the Java core library classes, for example, do produce a considerable number of these temporary objects, e.g., String, BigInteger, and Date. One common source of hidden objects is the + operator used to concatenate String objects. In his white paper, "WebSphere Application Server Development Best Practices for Performance and Scalability" (www-3.ibm.com/software/webservers/ appserv/ws_bestpractices.pdf), Harvey Gunther showed the high cost of this operator compared with the preferred method, which is to use a StringBuffer object and then call the append method. His measurements revealed that the total throughput when using + can be reduced by more than half using the preferred method.

    Another popular method call - which almost all objects use - is the toString() method. It is recommended that before using this method, you investigate how expensive the operation is going to be. If there is a cheaper way to do it - either by overriding the method or by using some other way - then use that method instead.

    Maxim 4: Release Objects as Soon as They Are No Longer Needed
    This maxim addresses the issue of memory leaks; it is also a mirror image of the second maxim, in which memory that was used but no longer needed still exists. It is when objects of this kind accumulate over time that the memory leak phenomenon happens. By setting a reference to null, the referent object becomes eligible for garbage collection. As soon as you know that the object is no longer needed, the null assignment should be done immediately. In other words, release the object as close as possible to the location where it was last used. The reasoning remains that holding on to too many objects causes more frequent and possibly longer garbage collections. Some of the common practices to avoid memory leaks also include the following:

  • Set nonstack variables to null as soon as their intended use is over.
  • Remove or clear objects from a collection when they are no longer needed. Understand the semantics of the methods available in the collection. For example, simply getting an object from a Hashtable does not release the reference to the object by the Hashtable. Instead, the clear() or remove() methods must be called.
  • Avoid objects that assume different states. When an object is in a given state, a set of fields is used to represent the state; when its state changes, a different set of fields is used. The danger comes when the values of the fields in the old state are left hanging after they are no longer needed. If you decide to represent many different states with one object, make sure to nullify fields that are no longer needed in the current state of the object.

    Maxim 5: Reuse Objects When It Helps
    Object reuse remains one of the elusive object-oriented practices today. Part of the reason is that reusing objects can break away from the rules of object-oriented design. However, there is an obvious and direct benefit of reusing objects.

  • Time: By reusing objects we avoid the actual creation of new objects and thereby save the cost of such operations. The cost varies depending on how large the class definition is and how specialized it is.
  • Space: By reusing objects, we are actually reusing the same memory space that has been allocated already. This results in a smaller memory footprint and consequently faster garbage collection.

    The main objective of reusing objects is to minimize the total cost incurred by creating objects by reducing the number of objects actually created. We present several ways.

  • Use of static classes: The easiest way to avoid creating objects is by creating static classes. Declaring a static class should be done with proper care, however, because this would imply that attributes are shared by all threads accessing the class, which might not be the appropriate semantics for the intended purpose of the class. On the other hand, it would help greatly if we could identify existing classes that can otherwise be declared static. For example, most utility classes do not need to have a unique object instance to do its job.
  • Use of Init and Cleanup methods: Most objects store information and references to other objects as part of their attributes. One of the reasons why some objects cannot be used easily is because they were created for a specific attribute value. A simple example is the java.lang.String class. A String object is immutable; that is, it cannot be set to a completely different string value without manipulating its current value. Thus, the only way to operate on a new string is to create another String object. We can achieve object reusability by introducing init and cleanup methods. The init method serves to reinitialize the values of the object's attributes. Optionally, the cleanup method is called to do any necessary cleaning up of the object before it can be reused. Ideally, each constructor of the class would have an equivalent init method. In this way, if an object ceases to serve its purpose, it can be reinitialized as needed.
  • Use of an Object Pool: Pooling objects is another way to reuse objects that is especially useful if the reusable objects are used in a larger scope within the application. A global pool is managed whereby threads can request object instances as well as return them to the pool. To be able to reuse the objects, an object would typically have methods similar to the init and cleanup methods described previously.

    One design scheme is to declare an interface, say Poolable, with a definition as shown in Listing 1.

    A class whose object instances need to be pooled should then implement this interface. The implementation allocates a pool and precreates objects of that class. The implementation can be made more sophisticated by automatically resizing (shrinking or expanding) the pool depending on the demand for object instances.

    Using an object pool dictates some programming methodology on the part of the developer. Instead of creating an object, you use a static call to the get method of the class and then initialize the object. Also, to actually benefit from reuse, the object must be returned explicitly to the pool using the return method.

    Maxim 6: Avoid Copying of Objects
    Another obvious - yet commonly ignored - source of performance problems is the practice of copying objects, especially deep copying from one component/layer to another, e.g., objects in a Result Set are copied by another component that uses them. Sometimes copying objects across components is the right way to go, depending on the semantics of the operations being done on these objects. Developers and designers, however, should carefully analyze whether these objects can be shared. If these objects are used in a read-only fashion, there is no reason to copy them. Be careful, however, that those objects are not refreshed or modified while they are still being used by other components.

    A careful inspection of the design is warranted and possibly requires redesigning the whole data interaction between components. Nevertheless, the main point is that for a medium-to-large set of objects, the savings in memory consumption can be very significant.

    Maxim 7: Avoid the Use of Finalizers
    We have heard many times that using finalizers is not a good idea. Every class has a finalizer method that can be overridden by the programmer. Typically, the finalizer contains code for cleaning up and releasing resources held by an instance of the class. When an object is to be garbage collected, the finalizer must be executed first. The semantics of Java do not guarantee when the finalizers are actually executed; it is possible that they are executed in the next garbage collection occurrence. In any case, the cost of executing the finalizers may be significant, especially when programmers misuse them to do more than cleaning up. This can potentially increase the duration of garbage collection. Finalizers are also prone to deadlock situations.

    Conclusion
    We have seen the cost of managing objects in the Java programming environment and learned that it can be very significant. Sometimes, application developers overlook the impact of creating too many objects and/or designing complex class structures on runtime performance. Most of the time they focus on pathlength and synchronization reduction. This article emphasizes the importance of looking at memory usage overhead and the effect of garbage collection on the overall performance of a J2EE application. Although advances in research and development are taking place, the garbage collection bottleneck remains a big challenge. Today JVMs are providing more and more methods and ways to tune garbage collection performance. It is our job to discover and use these methods. Also, application developers should learn good programming practices that alleviate the consumption of memory.

  • Comments (0)

    Share your thoughts on this story.

    Add your comment
    You must be signed in to add a comment. Sign-in | Register

    In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.