Advertisment

Smart object-management in Java : Part 2

author-image
CIOL Bureau
Updated On
New Update

The easiest way to reduce object creation in your programs is by using primitive types in place of objects. The primitive types in Java are boolean, byte, char, double, float, int, long, and short. When you create a variable of one of these types, there is no object creation overhead, and no garbage collection overhead when you're done using it. Instead, the JVM allocates the variable directly on the stack (if it's a local method variable) or within the memory used for the containing object (if it's a member variable).

Advertisment


Java defines wrappers for each of these primitive types.



Advertisment



The wrapper classes represent immutable values of the corresponding primitive types. They allow you to treat values of a primitive type as objects, and are very useful when you need to work with generic values that may be of any type. For instance, the standard Java class libraries define the java.util.Vector, java.util.Stack, and java.util.Hashtable classes for working with object collections. Wrapper classes provide a way to use these utility classes with values of primitive types

Advertisment




Advertisment

Except for such special cases, you're best off avoiding the usage of the wrapper classes and staying with the base types. This avoids both the memory and performance overhead of object creation.



Advertisment


Besides the actual wrapper types, other classes in the class libraries take values of primitive types and add a layer of semantics and behavior. Classes such as java.util.Date and java.awt.Point are examples of this type. If you're working with a large number of values of such types, you can avoid excessive object overhead by storing and passing values of the underlying primitive types, only converting the values into the full objects when necessary for use with methods in the class libraries. For instance, with the Point class you can access the internal int values directly, even combining them into a long so that a single value can be returned from a method call.



Advertisment



Reuse objects



Advertisment





Now let's consider another approach to reducing object churn: reusing objects. There are at least two major variations of this approach. The first technique -- dedicated object reuse -- has the advantages of simplicity and speed, while the second -- the free pool approach -- allows more efficient use of the objects.





Dedicated object reuse







The simplest case of object reuse occurs when one or more helper objects are required for handling a frequently repeated task. We'll use date formatting as an example, since it's a function that occurs fairly often in a variety of applications. To just generate a default string representation of a passed date value (represented as a long, let's say, given the preceding discussion of using primitive types), we can do the following:







        





__________________________________________________________________



        // Get the default string for time.


        long time = ...;


String display = DateFormat.getDateInstance().format(new Date(time));



___________________________________________________________________



        



This is a simple statement that masks a lot of complexity and object creation behind the scenes. The call to DateFormat.getDateInstance() creates a new instance of SimpleDateFormat, which in turn creates a number of associated objects; the call to format then creates new StringBuffer and FieldPosition objects. The total memory allocation resulting from this one statement actually came out to about 2,400 bytes, when measured with JRE 1.2.2 on Windows 98.





Since the program only uses these objects (except the output string) during the execution of this statement, there's going to be a lot of object churning if this use and discard approach is implemented in frequently executed code. Dedicated object reuse offers a simple technique for eliminating this type of churn.





Owned objects







By using some one-time allocations, we can create a set of objects required for formatting, then reuse these dedicated objects as needed. This set of objects is then owned by the code that uses them. For example, if we apply this approach using instance variables, so that each instance of the containing class owns a unique copy of the objects. This approach does incur some additional memory usage as the price of the speed advantage, since the objects used for the formatting are kept permanently allocated instead of being freed when not in use. But if the code is executed frequently, it's a very good trade-off.





Multithreading owned objects







If we have a set of owned objects, and multiple threads can execute the code that uses the objects concurrently, we need to prevent conflicts between the different threads' usage of the objects. The easiest way to accomplish this is to designate one of the objects as the lock for the whole set, and enclose the code using the set of objects within a synchronized block on the lock object. This adds the overhead of a locking operation for each use of the owned objects, but the locking overhead is low in comparison to the object creation time.





In this case, suppose we made the convertDate object the lock. The code fragment using the objects would then need to be changed to the following:







        // Get usage of the owned objects.


        synchronized (convertDate) {





            // Get the default string for time.


            long time = ...;


            convertDate.setTime(time);


            convertBuffer.setLength(0);


            StringBuffer output =


                dateFormatter.format(convertDate, convertBuffer, convertField);


            String display = output.toString();


        }









If the code using the owned objects is required to be single threaded, there's no need to bother with the locking step. However, adding the locking can give some additional flexibility.





For instance, the example we've been following uses instance variables for the owned objects, so that there's one set of objects for each instance of the containing class. This approach makes sense when there's heavy use of the owned objects, or when the objects may need to be configured differently for each instance of the containing class. This would be the case, for example, if we wanted to have the format string for our date example specified to the constructor of the class, instead of always using the default format. This approach gives the speed advantage of dedicated object reuse while sharing the memory overhead across all instances of the class





Pooled object reuse







Free object pools represent another form of object reuse. With the free pool approach, code-using objects of the pooled type must track usage and explicitly return the objects to the free pool when usage is complete. The free pool keeps a collection of objects available for reuse, adding returned objects to the collection. When an object is needed, the free pool removes one from the available collection and reinitializes it, rather than constructing a new object. The free pool only constructs a new object of the pooled type when the available collection is empty.





The bookkeeping overhead of maintaining a collection of available objects limits the performance gain of this approach, but it can still be useful in circumstances where there's a lot of reuse of a particular object type.





 



tech-news