If you read my last column, I noted that one major way to increase performance is to reuse objects:
"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 which 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."
Object reuse is important for a couple of reasons. First, the creation of an object is a costly operation in terms of memory allocation. As you know, the Garbage Collector in the Java Virtual Machine is responsible for memory management. In terms of object creation, the Garbage Collector is responsible for allocating the amount of memory required by an object. This means that the Garbage Collector must determine the amount of space required by the object and then allocate the space. Since the Java language supports inheritance, the memory requirement determination is achieved by "climbing" the inheritance tree and looking for member variables. Once the top of the tree is reached, the appropriate space is allocated. In order for the Garbage Collector to manage the memory, it needs to update the "memory table" (reference table). The "memory table" contains information about the memory (address space) and the number of references to that space (references to the object). This operation could logically be thought of as adding an entry to the table. Automatic memory management comes with a price; the Garbage Collector needs to maintain the memory table. This could be costly as the number of objects the Garbage Collector is responsible for grows. Performance hits also may be incurred when the Garbage Collector releases an object.
Secondly, the creation of an object is a costly operation in terms of execution speed. Not only does the JVM have to "climb" the inheritance tree to determine the appropriate amount of memory to allocate, it has to initialize the allocated memory. The object's memory initialization starts at the top of the inheritance tree and works down, calling the constructor of each class in the tree, and finishing with the instantiated object's constructor. For example, creating an instance of a
java.util.Vector
, requires the Garbage Collector to allocate enough memory for all of the member variables in each of its three parent classes (
java.util.AbstractList
,
java.util.AbstractCollection
,
java.lang.Object
). This memory is initialized by calling the constructor in each of the classes, starting with
java.lang.Object
and ending with
java.util.Vector
. Thus, the more complex the inheritance tree and the more complex the initialization logic, the more costly object creation becomes.
The following code listing rudimentarily demonstrates the amount of time spent in the creation of 100
java.lang.Object
and 100
java.util.Vector
objects.
import java.util.Vector;
public class CreationTest
{
private static long delta;
private static int iterations;
public static void main(String [] args)
{
if(args.length != 1)
{
System.out.println("usage: java CreationTest num_of_objects");
System.exit(-1);
}
try
{
iterations = Integer.parseInt(args[0]);
for(int i=0;i<iterations;i++)
{
long currentDelta = createObject();
delta += currentDelta;
}
long objectCreationDelta = delta;
delta = 0L;
for(int i=0;i<iterations;i++)
{
long currentDelta = createVector();
delta += currentDelta;
}
long vectorCreationDelta = delta;
System.out.println("The average time (in milliseconds) required to create
an Object is: " + (objectCreationDelta/iterations) );
System.out.println("The average time (in milliseconds) required to create
a Vector is: "+ (vectorCreationDelta/iterations) );
System.out.println("This was based on the creation of "+iterations+"
objects / vectors.");
System.exit(0);
}
catch(NumberFormatException nfe)
{
System.out.println("usage: java CreationTest num_of_threads");
System.exit(-1);
}
}
private static long createObject()
{
long currentTime = System.currentTimeMillis();
System.out.println("Current time before creating object: "+currentTime);
Object tmpObject = new Object();
long currentTimeToo = System.currentTimeMillis();
System.out.println("Current time after creating object: "+currentTimeToo);
long creationDelta = currentTimeToo - currentTime;
System.out.println("It took " + creationDelta
+ " milliseconds to create the object");
return creationDelta;
}
private static long createVector()
{
long currentTime = System.currentTimeMillis();
System.out.println("Current time before creating vector: "+currentTime);
Vector tmpVector = new Vector();
long currentTimeToo = System.currentTimeMillis();
System.out.println("Current time after creating vector: "+currentTimeToo);
long creationDelta = currentTimeToo - currentTime;
System.out.println("It took " + creationDelta
+ " milliseconds to create the vector");
return creationDelta;
}
}
|
It is important to realize that different implementations of virtual machines will have different results. It is also important that results may vary on different operating systems. Lastly, it is important to note that executing the code with or without a JIT (Just-In-Time compiler) could also affect performance. The results listed below are based on the classic VM (build JDK-1.2-V, native threads) version of the JVM running on Microsoft Windows® 98:
C:\kelby\articles\icthus\articles\threads>java CreationTest 100 Current time before creating object: 961475231130 Current time after creating object: 961475231190 It took 60 milliseconds to create the object Current time before creating object: 961475231190 Current time after creating object: 961475231240 It took 50 milliseconds to create the object Current time before creating object: 961475231240 Current time after creating object: 961475231300 It took 60 milliseconds to create the object Current time before creating object: 961475231350 Current time after creating object: 961475231350 . . . . . . . Current time before creating vector: 961475320880 Current time after creating vector: 961475320990 It took 110 milliseconds to create the vector Current time before creating vector: 961475320990 Current time after creating vector: 961475321050 It took 60 milliseconds to create the vector Current time before creating vector: 961475321050 Current time after creating vector: 961475321100 It took 50 milliseconds to create the vector Current time before creating vector: 961475321210 Current time after creating vector: 961475321270 It took 60 milliseconds to create the vector . . . . . . . The average time (in milliseconds) required to create an Object is: 17 The average time (in milliseconds) required to create a Vector is: 24 This was based on the creation of 100 objects / vectors. |
As you can see from above, object reuse (instead of object creation) is a good idea in almost every feasible case, from collections to implementations of event listeners. However, reuse is especially important when you are using objects that are associated with system resources such as sockets, streams (file handles), and threads. The creation of an object associated with a system resource is more costly than the creation of an object with no system resources. In fact, this type of object reuse is so important that in my last article I wrote:
"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."
Above, "cost" is defined in terms of execution speed. However, execution speed is not the only consideration when creating objects. Resource consumption (or resource "greediness") also defines the costliness of an operation. Every operating system has a limited number of sockets that can be created, a limited number of file handles that can be open at a given time, and a limited number of processes it can support. Therefore, developers must be mindful of the constraints design and implementation will place on the JVM, the solution, and the operating system.
True, there are some classes/operations that make reuse extremely hard, if not unattainable. For example, java.net.ServerSocket , as a result of a request [ accept() ], creates a new java.net.Socket . Therefore, a server that accepts 1000 requests will create 1000 sockets. In a solution where a server needs to be able to handle 1000 requests a second, the inherent and unavoidable object creation could be extremely costly. Using the object creation average of the java.lang.Object result noted above and assuming a new thread is created for every request, handling 1000 requests would waste 17 seconds in object creation. Therefore, in cases where reuse is unattainable, it is desirable to reuse the objects that "handle" the result. In the case of java.net.ServerSocket , it is desirable to reuse the object that handles the request.
Let us examine a simple TCP/IP-based multi-threaded server. The easiest way to create a multi-thread TCP/IP-based server is to create and spawn a new thread for every request. Though this design handles multiple requests at the same time, it has three major implications:
- The time required to create the thread affects the response time.
Using the object creation average of the java.lang.Object and assuming a new thread is created for every request, handling 1000 requests would waste 17 seconds in object creation. - The amount of memory consumed by the thread affects the manageability of the "memory table" and the availability of the memory associated with the virtual machine's heap.
- The constraint a thread may place on the underlying operating system could yield slow responses, crash the virtual machine as a result of running out of memory, or cripple the underlying operating system as a result of consuming all of the processes.
A much better design of a multi-threaded server is one that incorporates thread reuse, possibly through a thread pool. The use of a thread pool, and its inherent reuse of
java.util.AbstractList
s, could lessen the implications noted above. However, the simple use of a thread pool does not guarantee the alleviation of the implications. The underlying design of the thread pool (and the solution) could yield unfavorable results.
To demonstrate this claim, let us examine two different thread pool designs for a TCP/IP-based server. In both cases, the thread pool utilizes a queue (
Queue
) that contains objects waiting to be handled. In the designs explained below, the queue is implemented using a
Vector
. The
Queue
has a method to
add(_)
and
remove()
a
Runnable
. Realize that the queue and the solutions are over-simplified, allowing you to focus on the design.
The first solution follows a relatively simplistic design. The server (
Server
) creates a
ServerSocket
. Assuming this was successful, the server then creates the shared queue and the threads (
TPThread
) comprising the pool. The queue is passed to each of the threads via the constructor and then started [
start()
]. After the server performs the initializations, it waits for client requests [
accept()
]. Upon receiving the request, the
Socket
is wrapped into a
Runnable
(
BasicRunnable
) and then added to the queue. The
BasicRunnable
contains the logic required to handle the request. The "logic" sends a text message that contains a greeting and the visitor number. After the
Runnable
is added to the queue, a waiting thread is notified. The notified thread grabs the
Runnable
from the queue and invokes
run()
, "sending" a response back to the client. The code is listed below.
import java.net.*;
public class SimpleServer
{
private static final short port = 777;
public static void main(String args[])
{
try
{
//creates the ServerSocket and "tells" OS of its
//interest in requests comming to the specified port
ServerSocket serverSocket = new ServerSocket(port);
//create the Queue that holds the requests
Queue que = new Queue(5,10);
//create the Thread pool that contains 5 Threads
for(int i=0;I<5;i++)
{
Thread thread = new TPThread(que);
thread.start();
}
System.out.println("Server started properly");
//now let's wait and accept connections
while(true)
{
try
{
//get the request
Socket request = serverSocket.accept();
//create the runnable
Runnable runner = new BasicRunnable(request);
//add it to the que so a thread can handle it
que.add(runner);
}
catch(java.io.IOException ioe)
{
System.out.println("Problems handling request: " +
ioe);
}
}
}
catch(Exception e)
{
System.out.println("Problem starting the server....");
e.printStackTrace(System.out);
System.exit(-1);
}
}
}
|
import java.util.Vector;
public class Queue
{
private Vector theQue;
public Queue(int initialSize, int growthFactor)
{
theQue = new Vector(initialSize, growthFactor);
}
public synchronized void add(Runnable runner)
{
theQue.add(runner);
notify();
}
public synchronized Runnable next()
{
while(theQue.isEmpty())
{
try
{
wait();
}
catch(InterruptedException ie)
{
System.out.println("Could not wait()");
ie.printStackTrace(System.out);
}
}
Runnable runner = (Runnable) theQue.remove(0);
return runner;
}
}
|
import java.net.Socket;
import java.io.*;
public class BasicRunnable implements Runnable
{
private static int visitorNumber = 0;
private String greeting = "Welcome to the SimpleServer.";
private String response = "You are visitor number: ";
private final int myVisitorNumber;
private Socket connection;
public BasicRunnable(Socket s)
{
connection = s;
synchronized(BasicRunnable.class)
{
visitorNumber++;
myVisitorNumber = visitorNumber;
}
}
public void run()
{
try
{
OutputStream outStream =
connection.getOutputStream();
OutputStreamWriter output =
new OutputStreamWriter(outStream);
PrintWriter writer =
new PrintWriter(output);
writer.println(greeting);
writer.print(response);
writer.print(myVisitorNumber);
writer.print("\n");
writer.flush();
writer.close();
}
catch(Exception e)
{
System.out.println("Something weird happened: "+e);
e.printStackTrace(System.out);
}
}
}
|
public class TPThread extends Thread
{
private Queue que;
public TPThread(Queue que)
{
this.que = que;
}
public void run()
{
while(true)
{
Runnable runner = que.next();
runner.run();
}
}
}
|
Again, the goal of this article is to encourage the design of solutions that take advantage of object reuse. While examining our first design, ask yourself the question: "Does the design make good use of object reuse?"
First, the design could not attain reusability of the
Socket
object created as a result of the
ServerSocket.accept()
method. As noted above, if reuse is unattainable, you should try to create a design that reuses the handlers. In this design, we attempt to obtain object reuse through the two handlers, the
Thread
and the
Runnable
, which together handle the request. The design makes use of a queue shared by the threads in the thread pool. This allows us to design the thread in a manner that a single instance could be used to handle multiple requests. The thread, once started, grabs the next request from the queue, handles it, and then either waits or handles another request. In the context of the
Thread
handler, we have obtained our goal of reusing objects. In fact, we have also obtained the goal of reusing objects associated with system resources.
Now, consider the second handler, the
Runnable
. The design requires us to create a new
Runnable
for every request. Not only is a new
Socket
created for every request, a new
Runnable
is created for every request. So, "Does the design make good use of object reuse?" In the context of the second handler, we failed to meet our goal. In fact, the design of the second handler alleviated some of the implications above, while introducing new implications. The implications of dealing with the time required to create the thread, and the impact on the underlying OS, are no longer pertinent as this was taken care of by the thread pool. However, there are still the implications of dealing with object creation (creation of the
Runnable
) and the impact on the memory heap. A better design would be one where we could reuse the
Runnable
associated with the request. This would allow us to reuse the object associated with the system resource (the
Thread
) as well as the handler (the
Runnable
).
I noted at the start of this article that a common object reuse design pattern is to include a secondary initialization method beyond that of the constructor. This design pattern is visible in applets and servlets, as well as many other common object-oriented systems. Applets and servlets provide the secondary initialization functionality through the init(_) method. In the case of an applet, when the JVM in the browser "runs" the applet, it creates an instance of the class (by calling the constructor) and then initializes the applet by calling init() . If, and when, the JVM needs to restart the applet, instead of creating a new instance, the JVM reinitializes the applet by calling init() . This design alleviates the time incurred in the creation of an object while lessening the memory constraints placed on the browser.
Taking that pattern, let us apply it to our previous design. We have already concluded that we need to redesign the second handler, the
Runnable
. The goal of the new design is to reuse the second handler, instead of creating a new one for every request. Applying the pattern from above to our second handler, we could include in our implementation of the
Runnable
, an
init(Socket s)
method. This would allow us to associate a new
Socket
with an existing
Runnable
object. Every time a request is made, the server could grab an available
Runnable
, initialize it, and then hand it over to the thread pool. This would allow us to reuse both handlers.
Examining the source code below, you will notice that we have enhanced the
SimpleServer
to the
MediumServer
. The
MediumServer
contains much of the logic
SimpleServer
introduced with some modifications that incorporate the
Runnable
reuse. The
MediumServer
creates two instances of the
Queue,
the
runnableQue
and the
requestQue
. The
requestQue
contains
Runnable
objects that have been initialized and are ready to be handled. The
runnableQue
, initialized with
MediumRunnable
objects, contains multiple classes of runnable that are available for initialization. Once the queues are created and initialized, the server passes their references to each of the
TPThreadToo
instances. After the creation and initialization of the thread pool is complete, the server is ready to start accepting requests.
Upon receiving a request, the server grabs an available
Runnable
from the
runnableQue
and initializes it with the request's
Socket
. The server then will
add()
the initialized
Runnable
to the
requestQue
, allowing a thread in the thread pool to handle the request. When a thread from the thread pool obtains the
next()
request from the
requestQue
, the thread handles the request by calling
run()
on the
Runnable
. In order for the server to reuse the
Runnable
, the thread must
add(_)
the
Runnable
back into the
runnableQue.
import java.net.*;
public class MediumServer
{
private static final short port = 777;
public static void main(String args[])
{
try
{
//creates the ServerSocket and "tells" OS of its
//interest in requests coming to the
//specified port
ServerSocket serverSocket=new ServerSocket(port);
//create the Queue that holds the requests
Queue requestQue = new Queue(5,10);
//create the Queue that holds the runnables
Queue runnableQue = new Queue(10, 10,
MediumRunnable.class);
//create the Runnable pool that contains 10
//runnables
for(int i=0;i<10;i++)
{
Runnable runner = new MediumRunnable();
runnableQue.add(runner);
}
//create the Thread pool that contains 5 Threads
for(int i=0;i<5;i++)
{
Thread thread = new TPThreadToo(requestQue,
runnableQue);
thread.start();
}
System.out.println("Server started properly");
//now let's wait and accept connections
while(true)
{
try
{
//get the request
Socket request = serverSocket.accept();
//get a runnable
MediumRunnable runner = (MediumRunnable)
runnableQue.next();
//re-initialize the runner
runner.init(request);
//add it to the que so a thread can handle it
requestQue.add(runner);
}
catch(java.io.IOException ioe)
{
System.out.println("Problems handling request"
+ ioe);
}
}
}
catch(Exception e)
{
System.out.println("Problem starting the " +
"server....");
e.printStackTrace(System.out);
System.exit(-1);
}
}
}
|
import java.util.Vector;
public class Queue
{
private Vector theQue;
private Class runnableClass;
public Queue(int initialSize, int growthFactor)
{
theQue = new Vector(initialSize, growthFactor);
}
public Queue(int initialSize, int growthFactor, Class
runnable) throws ClassCastException
{
theQue = new Vector(initialSize, growthFactor);
//determine if the runnable is a sub-class of
//Runnable.class
if(Runnable.class.isAssignableFrom(runnable))
runnableClass = runnable;
else
throw new ClassCastException(runnable +
" is not a java.lang.Runnable sub-class");
}
public synchronized void add(Runnable runner)
{
theQue.add(runner);
if(runnableClass == null)
notify();
}
public synchronized Runnable next()
throws IllegalAccessException,InstantiationException
{
if(theQue.isEmpty() && runnableClass != null)
return (Runnable) runnableClass.newInstance();
while(theQue.isEmpty())
{
try
{
wait();
}
catch(InterruptedException ie)
{
System.out.println("Could not wait()");
ie.printStackTrace(System.out);
}
}
Runnable runner = (Runnable) theQue.remove(0);
return runner;
}
}
|
import java.net.Socket;
import java.io.*;
public class MediumRunnable implements Runnable
{
private static int visitorNumber = 0;
private String greeting = "Welcome to the " +
"MediumServer.";
private String response = "You are visitor number: ";
private int myVisitorNumber;
private Socket connection;
public MediumRunnable()
{}
public MediumRunnable(Socket s)
{
connection = s;
synchronized(MediumRunnable.class)
{
visitorNumber++;
myVisitorNumber = visitorNumber;
}
}
public void init(Socket s)
{
synchronized(MediumRunnable.class)
{
visitorNumber++;
myVisitorNumber = visitorNumber;
}
//reassign the socket
connection = s;
}
public void run()
{
try
{
OutputStream outStream =
connection.getOutputStream();
OutputStreamWriter output = new
OutputStreamWriter(outStream);
PrintWriter writer = new PrintWriter(output);
writer.println(greeting);
writer.print(response);
writer.print(myVisitorNumber);
writer.print("\n");
writer.flush();
writer.close();
}
catch(Exception e)
{
System.out.println("Something weird happened:"+e);
e.printStackTrace(System.out);
}
}
}
|
public class TPThreadToo extends Thread
{
private Queue requestQue, runnableQue;
public TPThreadToo(Queue reqQue, Queue runQue)
{
requestQue = reqQue;
runnableQue = runQue;
}
public void run()
{
while(true)
{
try
{
Runnable runner = requestQue.next();
runner.run();
runnableQue.add(runner);
}
catch(IllegalAccessException iae){}
catch(InstantiationException ie){}
}
}
}
|
Since the focus of this design was redesigning the second handler, let us examine it. Instead of creating a new
Runnable
for every request, a
Runnable
is taken from the
runnableQue
. (This removes the implications introduced in the previous design while addressing the implications associated with threads.) So, "Does the design make good use of object reuse?" In the context of both handlers, the design utilizes object reuse.
Possibly a more straightforward design would be to create a thread pool comprised of solution-specific implementations of a thread. This would allow you to remove the notion of reusing
Runnable
s objects, as the request (Socket) would be handed to the queue. The thread would then grab the socket and handle the request. Though this may be more straightforward, it does introduce reuse and maintenance issues for the components comprising your solution.
Hopefully, after reading this article you have encountered two very important guidelines for any Java solution: object creation can be a costly operation; and object reuse can be utilized to minimize that cost. Ideally the thread pool examples provided insight on how to incorporate thread reuse into a solution. The second thread pool example also introduced an implementation of a common object reuse design pattern: the secondary initializer. As developers moving forward, it is important to design Java-based solutions with performance in mind. Some simple design and implementation tricks that you can follow were discussed in this article and my previous article. However, it is important to realize that even the most well-thought designs and brilliant implementations can suffer in the performance arena. Therefore, before redesigning and re-implementing your solution, consider using a profiling tool!
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.
