Skip to main content

By clicking Submit, you agree to the developerWorks terms of use.

The first time you sign into developerWorks, a profile is created for you. Select information in your developerWorks profile is displayed to the public, but you may edit the information at any time. Your first name, last name (unless you choose to hide them), and display name will accompany the content that you post.

All information submitted is secure.

  • Close [x]

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerworks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

By clicking Submit, you agree to the developerWorks terms of use.

All information submitted is secure.

  • Close [x]

Tips and tricks for better Java performance, Part 2

Kelby Zorgdrager, Sr. Software Engineer, Sun Microsystems Inc.
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.

Summary:  This article addresses solution performance by discussing object reuse in a solution. It provides some basic examples that demonstrate the design of a solution which incorporates object reuse. The examples describe how object reuse can be achieved with system resources.

Date:  01 May 2001
Level:  Intermediate

Activity:  3847 views
Comments:  

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:

  1. 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.
  2. 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.
  3. 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!


About the author

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.

Report abuse help

Report abuse

Thank you. This entry has been flagged for moderator attention.


Report abuse help

Report abuse

Report abuse submission failed. Please try again later.


developerWorks: Sign in


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Select information in your developerWorks profile is displayed to the public, but you may edit the information at any time. Your first name, last name (unless you choose to hide them), and display name will accompany the content that you post.

Choose your display name

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


Rate this article

Comments

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Sample IT projects
ArticleID=10132
ArticleTitle=Tips and tricks for better Java performance, Part 2
publish-date=05012001
author1-email=
author1-email-cc=

Table of contents

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

For articles in technology zones (such as Java technology, Linux, Open source, XML), Popular tags shows the top tags for all technology zones. For articles in product zones (such as Info Mgmt, Rational, WebSphere), Popular tags shows the top tags for just that product zone.

For articles in technology zones (such as Java technology, Linux, Open source, XML), My tags shows your tags for all technology zones. For articles in product zones (such as Info Mgmt, Rational, WebSphere), My tags shows your tags for just that product zone.

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).