16.6.1 Memory
Memory deallocation is performed by a thread executing in the JVM called the garbage collector (GC).
All GCs sweep memory to identify objects that are no longer referenced. Most GCs use a compaction phase where referenced objects are copied to a particular area of memory to reduce fragmentation.
Creating an object consumes system resources, because the JVM must allocate memory and initialize the object. Similarly reclaiming memory using the garbage collector also uses resources, particularly CPU time. Garbage collection occurs asynchronously when free memory reaches threshold values, and it cannot be explicitly scheduled programmatically. A call to the System.gc() method will request that the JVM performs garbage collection. However, this is not guaranteed to happen immediately or within any specified time period.
The key to minimizing the performance impact of memory management is to minimize memory usage, particularly object creation and destruction. This can be achieved by a number of means:
- Object creation
Do not create objects prematurely if there is a possibility that they will not be needed. For example, if the object is only used in one path of an if statement, then create the object inside that path rather that outside the if statement - lazy initialization. If the same object can be reused inside a loop body, then declare and instantiate it outside the loop rather than inside the loop, to avoid creating and destroying a number of objects of the same class.
- Object pools
If objects of the same class are being repeatedly created and destroyed, it can be beneficial to create an object pool that allows the objects to be reused. Classes whose objects will be used in a pool need an initializer, so that objects obtained from the pool have some known initial state. It is also important to create a well-defined interface to the pool to allow control over how it is used.
IBM WAS V6 provides object pools for pooling application defined objects or basic JDK types. It will benefit an application which tries to squeeze every ounce of performance gain out of the system.
- Appropriate sizing for collections
Although the Java runtime environment will dynamically grow the size of collections such as java.util.Vector or Java.util.Hashtable, it is more efficient if they are appropriately sized when created. Each time the collection size is increased, its size is doubled so when the collection reaches a stable size it is likely that its actual size will be significantly greater than required. The collection only contains references to objects rather than the objects themselves, which minimizes the overallocation of memory due to this behavior.
- Temporary objects
Developers should be aware that some methods such as toString() methods can typically create a large number of temporary objects. Many of the objects may be created in code that you do not write yourself, such as library code that is called by the application.
- Use of static and final variables
When a value is used repeatedly and is known at compile time, it should be declared with the static and final modifiers. This will ensure that it will be substituted for the actual value by the compiler. If a value is used repeatedly but can be determined only at runtime, it can be declared as static and referenced elsewhere to ensure that only one object is created. Note that the scope of static variables is limited to the JVM. Hence if the application is cloned, care needs to be taken to ensure that static variables used in this way are initialized to the same value in each JVM. A good way of achieving this is the use of a singleton object. For example, an EJB initial context can be cached with a singleton using the following code fragment:
Example 16-1 Use of the singleton pattern to cache EJB initial context references
public class EJBHelper { private static javax.naming.InitialContext initialContext=null; public javax.naming.InitialContext getInitialContext() { if (initialContext = null) { initialContext = new javax.naming.InitialContext(); return initialContext } } }- Object references
Although memory does not have to be explicitly deallocated, it is still possible to effectively have "memory leaks" due to references to objects being retained even though they are no longer required. These objects are commonly referred to as loitering objects. Object references should be cleared once they are no longer required, rather than waiting for the reference to be implicitly removed when the variable is out of scope. This allows objects to be reclaimed sooner. Care should be taken with objects in a collection, particularly if the collection is being used as a type of cache. In this case, some criteria for removing objects from the cache is required to avoid the memory usage constantly growing. Another common source of memory leaks in Java is due to programmers not closing resources such as JDBC, JMS and JCA resources when they are no longer required, particularly under error conditions.
It is also important that static references be explicitly cleared when no longer required, because static fields will never go out of scope. Since WAS applications typically run for a long time, even a small memory leak can cause the JVM to run out of free memory. An object that is referenced but no longer required may in turn refer to other objects, so that a single object reference can result in a large tree of objects which cannot be reclaimed. The profiling tool available in IBM Rational Application Developer V6, which are described in Chapter 15, Development-side performance and analysis tools, can help to identify memory leaks. Other tools that can be used for this purpose include Rational PurifyŽ, Sitraka JProbe (by Quest Software), and Borland OptimizeIt.
- Vertical clustering
Most current garbage collection implementations are partially single threaded (during the heap compaction phase). This causes all other program threads to stop, potentially increasing the response times experienced by users of the application. The length of each garbage collection call is dependent on numerous factors, including the heap size and number of objects in the heap. Thus as the heap grows larger, garbage collection times can increase, potentially causing erratic response times depending on whether a garbage collection occurred during a particular interaction with the server. The effect of this can be reduced by using vertical scaling and running multiple copies of the application on the same hardware. Provided that the hardware is powerful enough to support vertical scaling, this can provide two benefits: first, the JVM for each member of the cluster will only require a smaller heap, and secondly, it is likely that while one JVM is performing garbage collection, the other one will be able to service client requests as the garbage collection cycles of the JVMs are not synchronized in any way. However, any client requests directed by workload management to the JVM (doing garbage collection) will be affected. Refer to 3.6, Vertical scaling topology for more information about vertical clustering.