Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

What's Object Pooling All About?


Aug00: Java Q&A

Alexandre is a computer engineering student at PUC-Rio in Brazil. He can be contacted at [email protected].


In languages such as C and C++, object allocation and deallocation can be either implicit or explicit (see Listing One). Allocation and deallocation occur implicitly when objects are declared as local variables in C++ functions. The compiler is then responsible for allocating the object, and also for deallocating it when the function returns. Explicit allocation and deallocation use the new and delete operators.

Java, on the other hand, has only one object allocation and deallocation policy. Objects are created explicitly with the new operator, but there is no corresponding delete operator. The release of unused memory always occurs implicitly. An object is deleted only when the garbage collector detects that there are no references to it.

Each of the two approaches has pros and cons. The two C++ approaches allow for more flexibility and tighter memory control. On the other hand, practice shows that lots of discipline and experience is necessary to avoid problems such as memory leaks and pointers to unallocated regions of memory.

Garbage collection solves the aforementioned problems. On the downside, garbage-collected languages have a tendency to use more memory, on average, than languages that allow explicit deallocation. Another disadvantage is that the execution of the garbage collector introduces random pauses during the lifetime of applications.

The typical Java application simply uses all the memory available in its heap until no more allocations can be made. At that point, the Java run time executes the garbage collector to free as much memory as possible. If not enough memory is obtained to honor the allocation request, the heap size is increased.

The execution time of the garbage collector is directly proportional to the number of currently allocated objects in the heap. During garbage collection, the application is completely frozen. In addition, applications in which precise timing plays an important role, such as multimedia or real-time programs, suffer greatly from the unpredictable pauses introduced by garbage collection. Applications that keep data structures with a large number of allocated objects in memory are slowed down, as the garbage collector takes a long time to walk the heap on each execution.

Object Pools

The use of object pools in Java aims to reduce the number of garbage-collection runs during the lifetime of a Java application.

There are several classes in the Java standard library and on most applications with objects that could be reused instead of left in the heap waiting for the garbage collector. Two examples are java.awt.geom .Point2D.Double and java.awt.geom.Rectangle2D.Double.

Consider the two Java methods in Listing Two. The readRectangle method allocates two temporary instances of the Point2D.Double class that will never be used outside the scope of the function. Every time the readRectangle method is called, it will add two unused objects to the heap, which will eventually become full. This will force the Java Runtime to execute the garbage collector.

Now imagine that the same two instances were used in every execution of the method, as in Listing Three. This would prevent the method from contributing to the execution of the garbage collector. On the other hand, if the readRectangle method is rarely or never used, you would have two unused points in memory for every instance of GeomParser. A single, centralized pool of reusable objects would solve the problem much more efficiently.

The tecgraf.objpool Package

My proposed solution to these problems is the ObjectPool class, contained in the tecgraf.objpool package (available electronically; see "Resource Center," page 5). It is a centralized, thread-safe repository of reusable objects of any class.

Unlike most object pool implementations, ObjectPool can be extended to handle all the classes you need. Most implementations force you to keep a different pool for each class you want to manage, which is unwieldy.

Internally, each pooled class has its own manager, which is a subclass of ClassManager. The class manager keeps a list of available objects, and uses those objects in reply to allocation requests. If an allocation is requested and the list is empty, a new instance is created by calling the abstract newInstance method. This new object is then used to fulfill the request. Released objects are simply added to the list of available objects.

Listing Five is an example class manager for the java.awt.geom.Point2D.Double class. It implements the newInstance method to return a newly allocated instance of java.awt.geom.Point2D.Double.

It it not necessary, though, to create new subclasses of ClassManager for every class handled by ObjectPool. There is a good portion of reusable classes that provide constructors with no parameters, also called "default constructors." For those classes, the ReflectionClassManager provides a working implementation of the newInstance method.

If the constructor desired for instantiating the class has one or more parameters, it can still be used by providing the parameter types and values to ReflectionClassManager, allowing for even more flexibility.

When an allocation request is issued, ObjectPool checks if a ClassManager is installed for the given class name. If none is found, a new ReflectionClassManager will be automatically installed, based on the assumption that the class has a constructor with no parameters.

Performance Evaluation

Figure 1 shows the compared memory usage along the execution of the same test program when ObjectPool and new are used for memory allocation. It is clear that, in this test scenario, the use of the object pool prevented all executions of the garbage collector, keeping a uniform memory usage during the lifetime of the program. The test program used for the gathering of this data executes a function that allocates four temporary objects and then releases them, such as the readRectangle example in Listings One and Four.

Allocating and releasing objects in ObjectPool are both O(1) operations. The objects are used in a LIFO (last in, first out) order, as if in a stack. The principle of locality mandates that the last object added to the free list is probably the most recently used one as well. In practical terms, this means it is more likely to be on cache memory and less likely to be on secondary storage (swap area). It is only logical, then, to use that object first for the sake of performance.

Calling ObjectPool.allocate and ObjectPool.release is only slightly slower than using new by a constant factor. This difference, as measured in the test machines, is in the order of 0.1 milliseconds per allocation. This delay is due to the fact that new is implemented directly by the Java Runtime, in optimized native code, as opposed to a Java class. Applications negatively affected by the garbage collector executions, however, will find that using ObjectPool will more than make up for this difference.

One weakness of the ObjectPool class is that, when a large number of threads are performing simultaneous memory allocations of the same class, there is a serious delay due to monitor contention on the ClassManager instance. The mapping of class names to ClassManager instances, implemented as a java.util.HashMap in ObjectPool, is another major source of monitor contention. Figure 2 shows a comparison of the execution time of the test application when an increasing number of threads is used, with ObjectPool and new.

The test in Figure 2 shows a weakness in the synchronization instance returned by java.util.Collections.synchronizedMap_it only allows one thread to access the map at a time. The vast majority of accesses to the map in ObjectPool are read accesses, which could be performed in parallel. Because the synchronization is based on mutual exclusion, there is a serious performance penalty as the number of threads increases.

It must be taken into consideration, however, that the threads used for this test spent practically all of their time allocating and releasing objects of the same class, which contributed to a somewhat unrealistic worst-case scenario. In a real-life application, the threads would spend most of their time doing their actual work, so the chance of two threads trying to allocate or release objects simultaneously would drop considerably.

Figures 3 and 4 compare the average and peak memory usages for an increasing number of threads when using ObjectPool and new. This comparison lets you confirm that the number of threads does not affect the benefits of reduced memory usage brought by object pooling.

Conclusion

Object pooling can be used very effectively to reduce the number of garbage collection executions in a Java application. This reduction may bring benefits in terms of reduced processor usage and the absence of the unpredictable delays introduced by garbage collection. Object pooling can also lower the average memory usage of an application.

On the other hand, object pools can bring to Java the classic problem of continuing to reference a released object. This forces the programmer to be more careful and thus nullifies one of the benefits of garbage collection for the sake of performance.

DDJ

Listing One

class A {
   public:
          int i;
};
// Implicit memory allocation and deallocation.
void function1 ()
{
          A a;
          a.i = 0;
}
// Explicit memory allocation and deallocation.
void function2 ()
{
          A *a = new A();
          a->i = 0;
          delete a;
}

Back to Article

Listing Two

import java.io.*;
import java.awt.geom.*;

public class GeomParser
{
          // Reads a point from the given input stream and stores it in
          // parameter 'pt'.
          public void readPoint( DataInputStream dis, Point2D pt )
          {
                    double x, y;
                    x = dis.readDouble();
                    y = dis.readDouble();
                    pt.setLocation(x, y);
          }
          // Reads a rectangle from the given input stream and stores it in
          // parameter 'rect'.
          public void readRectangle( DataInputStream dis, Rectangle2D rect )
          {
                    Point2D p1 = new Point2D.Double(),
                             p2 = new Point2D.Double();
                    readPoint(dis, p1);
                    readPoint(dis, p2);

                    rect.setRect(0.0, 0.0, 0.0, 0.0);
                    rect.add(p1);
                    rect.add(p2);
          }
}

Back to Article

Listing Three

import java.io.*;
import java.awt.geom.*;

public class GeomParser
{
          Point2D p1 = new Point2D.Double();
          Point2D p2 = new Point2D.Double();

          // Reads a point from the given input stream and stores it in
          // parameter 'pt'.
          public void readPoint( DataInputStream dis, Point2D pt )
          {
                    double x, y;
                    x = dis.readDouble();
                    y = dis.readDouble();
                    pt.setLocation(x, y);
          }
          // Reads a rectangle from the given input stream and stores it in
          // parameter 'rect'.
          // Method has to be made synchronized to avoid thread contention
          // over objects 'p1' and 'p2'.
          public synchronized void readRectangle( DataInputStream dis, 
                                                           Rectangle2D rect )
          {
                    readPoint(dis, p1);
                    readPoint(dis, p2);

                    rect.setRect(0.0, 0.0, 0.0, 0.0);
                    rect.add(p1);
                    rect.add(p2);
          }
}

Back to Article

Listing Four

import java.io.*;
import java.awt.geom.*;

public class GeomParser
{
          // Reads a point from the given input stream and stores it in
          // parameter 'pt'.
          public void readPoint( DataInputStream dis, Point2D pt )
          {
                    double x, y;
                    x = dis.readDouble();
                    y = dis.readDouble();
                    pt.setLocation(x, y);
          }
          // Reads a rectangle from the given input stream and stores it in
          // parameter 'rect'.
          public void readRectangle( DataInputStream dis, Rectangle2D rect )
          {
                    Point2D p1, p2;
                    // Obtain instances from object pool.
                    p1 = (Point2D) 
                         ObjectPool.allocate("java.awt.geom.Point2D$Double"),
                    p2 = (Point2D) 
                         ObjectPool.allocate("java.awt.geom.Point2D$Double");
                    readPoint(dis, p1);
                    readPoint(dis, p2);

                    rect.setRect(0.0, 0.0, 0.0, 0.0);
                    rect.add(p1);
                    rect.add(p2);

                    // Objects are no longer needed, release them.
                    ObjectPool.release(p1);
                    ObjectPool.release(p2);
          }
}

Back to Article

Listing Five

import java.awt.geom.*;
import tecgraf.objpool.*;

public final class Point2D_Double_ClassManager
extends ClassManager
{
          private Point2D_Double_ClassManager()
          {
                    super();
          }
          public Object newInstance()
          {
                    return new Point2D.Double();
          }
          static {
                    // Installs class manager for java.awt.geom.Point2D.Double
                    ObjectPool.setClassManager(
                             "java.awt.geom.Point2D$Double",
                             new Point2D_Double_ClassManager()
                    );
          }
}

Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.