J2EE application performance optimization

How to extract maximum performance from your J2EE Web applications

In today's world of larger-then-ever software applications, users still expect real-time data and software that can process data at blazing speeds. With the advent of broadband access, users have grown accustomed to receiving responses in seconds, irrespective of how much data the server handles. It is becoming increasingly important for Web applications to provide as short response times as possible. The most obvious and simple way to improve a Website's performance is by scaling up hardware. But scaling the hardware does not work in all cases and is definitely not the most cost-effective approach. Other solutions can improve performance, without extra costs for the hardware. This article provides some suggestions that prove helpful when trying to maximize a J2EE Web application's performance.

If you decide to try this article's recommendations, keep in mind this article provides suggestions only. Performance tuning is as much an art as it is a science. Changes that often result in improvement might not make any difference in some cases, or, in rare scenarios, they can result in degradation. For best results with performance tuning, take a holistic approach.

Overview

Figure 1 illustrates at a broad level how a J2EE application appears when deployed in a production environment. To get the best performance from a J2EE application, all the underlying layers must be tuned, including the application itself.

Figure 1. J2EE application architecture. Click on thumbnail to view full-sized image.

For maximum performance, all the components in Figure 1—operating system, Web server, application server, and so on—need to be optimized. This article will give a glimpse into tuning a J2EE application server, Web server, relational database, and your J2EE application. For maximum payoff from this article, follow these guidelines:

  • Set a goal: Before you begin tuning your J2EE application's performance, set a goal. Often this goal addresses the maximum concurrent users the application will support for a given limit on response times. But the goal can also focus on other variables—for example, the response times should not increase more than 10 percent during the peak hour of user load.
  • Identify problem areas: It is important to identify the bottlenecks when you start making changes to improve performance. A little investigation into problems might reveal the specific component that causes poor performance. For example, if the CPU usage on an application server is high, you will want to focus on tuning the application server first.
  • Follow a methodical and focused path: Once the goal is set, try to make changes that are expected to have the biggest impact on performance. Your time is better spent tuning a method that takes 10 seconds but gets called 100 times than tuning a method that takes one minute but gets called only once. In an ideal world, you test one change at a time before using it in a production environment. You make one change and stress-test it. If the change results in positive impact, only then will you make it permanent.

Identify bottlenecks

The goal of performance tuning is to identify bottlenecks and remove them. It is an iterative process. Once one area of the application improves, another area will become a bottleneck. You must repeat the cyclic process of first identifying the bottleneck, then resolving the bottleneck, then identifying the next bottleneck until the desired goal has been reached. You will need two kinds of tools that prove helpful in this process. First, you need stress tools that generate load for your application. Second, you need monitoring tools that collect data for various performance indicators.

Stress tools

Many different stress tools are available in the market today. Some of the popular ones are:

  • Mercury Interactive's LoadRunner
  • Segue's SilkPerformer
  • RadView Software's WebLoad

For a comprehensive list of stress tools see this article's Resources section. You must choose a tool that best fits your needs based on the tool's features and associated price tag. Some of the important features you should consider before choosing a tool are:

  • Support for a large number of concurrent Web users, each running through a predefined set of URL requests
  • Ability to record test scripts automatically from browser
  • Support for cookies, HTTP request parameters, HTTP authentication mechanisms, and HTTP over SSL (Secure Socket Layer)
  • Option to simulate requests from multiple client IP addresses
  • Flexible reporting module that lets you specify the log level and analyze the results long after the tests were run
  • Option to specify the test duration, schedule test for a later time, and specify the total load

For the sample tests mentioned in this article, LoadRunner was used.

Performance monitors

Using a monitoring tool, you collect data for various system performance indicators for all the appropriate nodes in your network topology. Many stress tools also provide monitoring tools. Windows OS also has a built-in performance monitor sufficient for many purposes. This Windows performance monitor can be started from the Administrative Tools menu, accessed from the Control Panel menu, or by typing "perfmon" in the Run window (accessed from the Start menu). You can display the performance counters data in real time, but usually, you'll want to log this data into a file so it can be viewed later. To log the data into a file, go to the Counter Logs selection in the left-hand side of the Performance window, right click with your mouse, and select New Log Settings as shown in Figure 2.

Figure 2. Windows performance monitor. Click on thumbnail to view full-sized image.

You can also set the file path/name for where the data should be logged, as well as a schedule of when to collect this data. It is possible to log the data and view it in real time, but the performance counters must be added twice—first, in the System Monitor link on the left-hand side and second, in Counter Logs, as shown above.

Many performance counters are available in Windows OS. The following table lists some of the important counters that you should always monitor:

System resourcePerformance monitor Description
CPUSystem: Processor queue lengthProcessor queue length indicates the number of threads in the server's processor queue waiting to be executed by the CPU. As a rule of thumb, if the processor queue remains at a value higher than 2 for an extended period of time, most likely, the CPU is a bottleneck.
Processor: Percent of processor timeThis counter provides the total overall CPU utilization for the server. A value that exceeds 70-80 percent for long periods indicates a CPU bottleneck.
System: Percent of total privileged timeThis counter measures what percentage of the total CPU execution time is used for running in kernel/privileged mode. All the I/O operations run under kernel mode, so a high value (about 20-30 percent) usually indicates problems with the network, disk, or any other I/O interface.
RAMMemory: Pages per secondPages per second is the number of pages read from or written to the disk for resolving hard page faults. A value that continuously exceeds 25-30 indicates a memory bottleneck.
Memory: Available bytesAvailable bytes is the amount of physical memory available to processes running on the computer, in bytes. A low value (less than 10 MB) usually means the machine requires more RAM.
Hard diskPhysical disk: Average disk queue lengthThe average number of requests queued for the selected disk. A sustained value above 2 indicates an I/O bottleneck.
Physical disk: Percent of disk timeThe percentage of elapsed time that the selected disk drive is busy servicing requests. A continuous value above 50 percent indicates a bottleneck with hard disks.
NetworkNetwork interface: Total bytes per secondShows the bytes transfer rate (sent and received) on the selected network interface. Depending on the network interface bandwidth, this counter can tell if the network is the problem.
Network interface: Output queue lengthIndicates the length of an output packet queue in packets. A sustained value higher than 2 indicates a bottleneck in the network interface.

You should add the above counters (and any others as appropriate) in your counter log and collect this data while you stress-test your application using the stress tool. A file generated by the counter log can be opened later by clicking on the View Log File Data button on the right-hand side toolbar. Looking at these counters should give you some hint as to where the problem exists—application server, Web server, or database server.

After identifying the bottleneck this way, you should try to resolve it. You can use two different strategies—either tune the hosting environment in which your application runs or tune the application itself.

Environment tuning

In this section, we look at the possible tuning options in a typical J2EE Web application hosting environment. As already discussed above, a J2EE application environment usually consists of an application server, Web server, and a backend database.

Web server/application server tuning

Most application servers and Web servers provide similar kinds of configuration options though they have different mechanisms to set them. In this article, I cover Tomcat 4.1.x and Apache 1.3.27, which talk to each other using the JK connector. The configuration options presented here exist in most of the other application servers and Web servers, but you will need to locate the correct place to set them.

Apache

Probably the most important setting for your Windows Apache HTTP Server is the option for number of threads. This value should be high enough to handle the maximum number of concurrent users, but not so high that it starts adding its own overhead of too many context switches. The optimum value can be determined by monitoring the number of threads in use during peak hours. To monitor the threads in use, make sure you have the following configuration directives present in the Apache configuration file (httpd.conf):

LoadModule status_module modules/mod_status.so
<Location /server-status>
    SetHandler server-status
    Allow from all
</Location>

Now, from your browser, make an HTTP request to your Apache server with this URL: http://<apache_machine>/server-status. It displays how many requests are being processed and their status (reading request, writing response, etc.). Monitor this page during peak load on the server to ensure the server is not running out of idle threads. After you come up with the optimum number of threads for your application, change the ThreadsPerChild directive in the configuration file to an appropriate value.

A few other items that improve performance in the Apache HTTP Server are:

  • DNS reverse lookups are inefficient. Switch off DNS lookups by setting HostnameLookups in the configuration file to off.
  • Do not load unnecessary modules. Apache allows dynamic modules that extend basic functionality of the Apache HTTP Server. Comment out all the LoadModule directives you don't need.
  • Try to minimize logging as much as possible. Look for directives LogLevel, CustomLog, and LogFormat in the configuration file for changing logging level.
  • Minimize the JK connector's logging also by setting the JkLogLevel directive to emerg.

Tomcat

The two most important configuration options for Tomcat are its heap size and number of threads. Unfortunately there is no good way to determine the heap size needed because in most cases, the JVM doesn't start cleaning up the memory until it reaches the maximum memory allocated. One good rule of thumb is to allocate half of the total physical RAM as Tomcat heap size. If you still run out of memory, look into your application design to reduce memory usage, identify any memory leaks, or try various garbage collector options in the JVM. To change the heap size add -Xms<size> -Xmx<size> as the JVM parameter in the command line that starts Tomcat. <size> is the JVM heap size usually specified in megabytes by appending a suffix m, for example, 512m. Initial heap size is -Xms, and -Xmx is the maximum heap size. For server applications, both should be set to the same value.

The number of threads in Tomcat can be modified by changing the values of minProcessors and maxProcessors attributes for the appropriate connector in <Tomcat>/conf/server.xml. If you are using the JK connector, change the values of its attributes. Again, there is no simple way to decide the optimum value for these attributes. The value should be set such that enough threads are available to handle your Web application's peak load. You can monitor a process's current thread count in the Windows Task Manager, which can assist in determining the correct value of these attributes.

A few other options you should be aware of:

  • If you are not using the latest JRE (Java Runtime Environment), consider upgrading to the latest one. I have seen up to a 30 percent performance improvement after upgrading from JRE 1.3.1 to JRE 1.4.1.
  • Add the -server option to the JVM options for Tomcat, which should result in better performance for server applications. Note that this option, in some cases, causes the JVM to crash for no apparent reason. If you face this problem, remove the option.
  • Change the default Jasper (JavaServer Pages, or JSP, compiler) settings in <Tomcat>/conf/web.xml by setting development="false", reloading="false" and logVerbosityLevel="FATAL".
  • Minimize logging in Tomcat by setting debug="0" everywhere in <Tomcat>/conf/server.xml.
  • Remove any unnecessary resources from the Tomcat configuration file. Some examples include the Tomcat examples Web application and extra <Connector>, <Listener> elements.
  • Set the autodeploy attribute of the <Host> tag to false (unless you need any of the default Tomcat applications like Tomcat Manager).
  • Make sure you have set reloadable="false" for all your Web application contexts in <Tomcat>/conf/server.xml.

Database tuning

In the case of Microsoft SQL Server, more often than not, you do not need to modify any configuration options, since it automatically tunes your database to a great degree. You should change these settings only if your stress tests identify the database as a bottleneck. Some of the configuration options that you can try are:

  • Run your SQL Server on a dedicated server instead of a shared machine.
  • Keep your application database and your temporary database on different hard disks.
  • Consider taking local backups and moving them to a different machine. The backups should complete much faster.
  • Normalize your database to the third normal form. This is usually the best compromise, as the fourth and fifth forms of normalization can result in performance degradation.
  • If you have more than 4 GB of physical RAM available, set the awe enabled configuration option to 1, which will allow SQL Server to use more than 4 GB of memory up to a maximum of 64 GB (depending on the SQL Server edition).
  • In case you have many concurrent queries executing and enough memory is available, you can increase the value of the min memory per query option (default is 1,024 KB).
  • Change the value of the max worker threads option, which indicates the maximum number of user connections allowed. Once this limit is reached, any new user requests will wait until one of the existing worker threads finishes its current task. The default value for this option is 255.
  • Set the priority boost option to 1. This will allow SQL Server to run with a higher priority as compared to the other applications running on the same server. If you are running on a dedicated server, it is usually safe to set this option.

If none of the configuration options resolve the bottleneck, you should consider scaling up the database server. Horizontal scaling is not possible in SQL Server, as it does not support true clustering, so usually, the only option is vertical scaling.

Application tuning

After you have tuned your hosting environment, now it is time to get down and dirty inside your application source code and database schema. In this section, we look at one of the many possible ways to tune your Java code and your SQL queries.

Java code optimization

The most popular way to optimize Java code is by using a profiler. Sun's JVM has built-in support for profiling (Java Virtual Machine Profiler Interface, or JVMPI) that can be switched at execution time by passing the right JVM parameters. Many commercial profilers are available; some rely on JVMPI, others provide their own custom hooks into Java applications (using bytecode instrumentation or some other method). But be aware that all these profilers add significant overhead. Thus, your application cannot be profiled at a realistic load level. Use these profilers with a single user or a limited number of users. It is still a good idea to run your application through the profiler and analyze the results for any obvious bottlenecks.

To identify your application's slowest areas in a full-fledged deployed environment, you can add your own timing logs to the application, which can be switched off easily in the production environment. A logging API, such as log4j or J2SE 1.4's Java Logging API, is handy for this purpose. The code below shows a sample utility class that can help you add timing logs to your application:

import java.util.HashMap;
//Import org.apache.log4j.Logger;
public class LogTimeStamp {
    private static HashMap ht = new HashMap();
    // Preferably we should use log4j instead of System.out
//    private static Logger logger = Logger.getLogger("LogTimeStamp");
    private static class ThreadExecInfo {
        long timestamp;
        int stepno;
    }
    public static void LogMsg(String Msg) {
        LogMsg(Msg, false);
    }
    /*
     * Passing true in the second parameter of this function resets the counter for
     * the current thread. Otherwise it keeps track of the last invocation and prints
     * the current counter value and the time difference between the two invocations.
     */
    public static void LogMsg(String Msg, boolean flag) {
        LogTimeStamp.ThreadExecInfo thr;
        long timestamp = System.currentTimeMillis();
        synchronized (ht) {
            thr = (LogTimeStamp.ThreadExecInfo) ht.get(Thread.currentThread().getName());
            if (thr == null) {
                thr = new LogTimeStamp.ThreadExecInfo();
                ht.put(Thread.currentThread().getName(), thr);
            } 
        }
        if (flag == true) {
            thr.stepno = 0;
        }
        if (thr.stepno != 0) {
//            logger.debug(Thread.currentThread().getName() + ":" + thr.stepno + ":" +
//                    Msg + ":" + (timestamp - thr.timestamp));
            System.out.println(Thread.currentThread().getName() + ":" + thr.stepno + ":" +
                    Msg + ":" + (timestamp - thr.timestamp));
        }
        thr.stepno = thr.stepno + 1;
        thr.timestamp = timestamp;
    }
}

After adding the above class in your application, you must invoke method LogTimeStamp.LogMsg() at various checkpoints in your code. This method prints the time (in milliseconds) it took for one thread to get from one checkpoint to the next one. First, call LogTimeStamp.LogMsg("Your Msg", true) at one place in the code that is the start of a user request. Now you can insert the following invocations in your code:

    public void startingMethod() {
        ...
        LogTimeStamp.LogMsg("This is a test message", true); //This is starting point
        ...
        LogTimeStamp.LogMsg("One more test message"); //This will become check point 1
        method1();
        ...
    }
    public void method1() {
        ...
        LogTimeStamp.LogMsg("Yet another test message"); //This will become check point 2
        method2();
        ...
        LogTimeStamp.LogMsg("Oh no another test message"); //This will become check point 4
    }
    public void method2() {
        ...
        LogTimeStamp.LogMsg("Wow! another test message"); //This will become check point 3
        ...
    }

The Perl script analyze.pl, which can be downloaded from Resources, can take the output of the above log messages as input and print the results in the format below. From these results, you now know which part of the code requires the most time and can concentrate on optimizing that part:

Transactions                           Avg. Time  Max Time  Min Time
--------------------------------------------------------------------
[This is a ...] to [One more t...]         14410     20937      7500
[One more t...] to [Yet anothe...]            16        62         0
[Yet anothe...] to [Wow! anoth...]         39860     50844     27703
[Wow! anoth...] to [Oh no anot...]           711      1844        94
[Oh no anot...] to [OK thats e...]         68089    228452     19718

The above approach represents just one of the ways to tune your Java code. You can use whatever methodology works for you. Some excellent resources are available for Java performance tuning. Check Resources for some links. Some general suggestions you should be aware of while developing a J2EE application are:

  • Avoid using synchronized blocks in your code as much as possible. That does not mean you should abdicate handling synchronization for your code's multithreaded parts, but you should try to limit its usage. Synchronized blocks can severely impair your application's scalability.
  • Proper logging proves necessary in serious software development. You should try to use a logging mechanism (like log4j) that lets you switch off logging in the production environment to reduce logging overhead.
  • Instead of creating and destroying resources every time you need them, use a resource pool for every resource that is costly to create. One obvious choice for this is your JDBC (Java Database Connectivity) Connection objects. Threads are also usually good candidates for pooling. Many free APIs are available for pooling various resources.
  • Try to minimize the objects you store in HttpSession. Extra objects in HttpSession not only lead to more memory usage, they also add additional overhead for serialization/deserialization in case of persistent sessions.
  • Where ever possible, use RequestDispatcher.forward() instead of HttpServletResponse.sendRedirect(), as the latter involves a trip to the browser.
  • Minimize the use of SingleThreadModel in servlets so that the servlet container does not have to create many instances of your servlet.
  • Java stream objects perform better than reader/writer objects because they do not have to deal with string conversion to bytes. Use OutputStream in place of PrintWriter.
  • Reduce the default session timeout either by changing your servlet container configuration or by calling HttpSession.setMaxInactiveInterval() in your code.
  • Just as we switched off the DNS lookup in the Web server configuration, try not to use ServletRequest.getRemoteHost(), which involves a reverse DNS lookup.
  • Always add directive <%@ page session="false"%> to JSP pages where you do not need a session.
  • Excessive use of custom tags also may result in poor performance. Keep performance in mind while designing your custom tag library.

SQL query optimization

The optimization of SQL queries is a vast subject in itself, and many books cover only this topic. SQL query running times can vary by many orders of magnitude even if they return the same results in all cases. Here I just show how to identify the slow queries and offer a few suggestions as to how to fix some of the most common mistakes.

First of all, to identify slow queries, you can use SQL Profiler, a tool from Microsoft that comes standard with SQL Server 2000. This tool should be run on a machine other than your SQL Server database server and the results should be stored in a different database as well. Storing results in a database allows all kinds of reports to be generated using standard SQL queries. Profiling any application inherently adds a lot of overhead, so try to use appropriate filters that can reduce the total amount of data collected.

To start the profiling, from SQL Profiler's File menu, select New, then Trace, and give the connection information and the appropriate credentials to connect to the database you want to profile. A Trace Properties windows will open, where you should enter a meaningful name you will recognize later. Select Save To Table option and now give the connection information and credentials for the database server (this should differ from the server you are profiling) where you want to store the data collected by the profiler. Next, you should be asked to provide the database and the table name where the results will be stored. Usually, you would also want to add a filter, so go to the Filters tab and add the appropriate filters (for example "duration greater than or equal to 500 milliseconds" or "CPU greater than or equal to 20" as shown in Figure 3). Now click on the Run button and the profiling will start.

Figure 3. SQL Profiler. Click on thumbnail to view full-sized image.

Let's say based on SQL Profile's results, you have identified the following query as the most time consuming:

SELECT [TABLE1].[T1COL1], [TABLE1].[T1COL2], 
       [TABLE1].[T1COL3], [TABLE1].[T1COL4]
FROM ((([TABLE1] LEFT JOIN [TABLE4] ON [TABLE1].[T1COL4] = [TABLE4].[T4COL4]) 
        LEFT JOIN [TABLE3] ON [TABLE1].[T1COL3] = [TABLE3].[T3COL3]) 
        LEFT JOIN [TABLE2] ON [TABLE1].[T1COL2] = [TABLE2].[T2COL2]) 
WHERE [TABLE1].[T1COL5] = 'VALUE1'

Now your task is to optimize this query to improve performance. You look at your database and find that TABLE1 has 700,000 records, TABLE2 has 16, TABLE3 has 100, and TABLE4 happens to have more than 4 million records. You should first understand this query's cost, and Query Analyzer comes in handy for this task. Select Show Execution Plan in the Query menu and execute this query in Query Analyzer. Figure 4 shows the resulting execution plan.

Figure 4. Execution plan before indexes. Click on thumbnail to view full-sized image.

From this plan, you see that SQL Server is clearly doing full-table scans for all four tables, and together, they make up around 80 percent of the total query cost. Luckily, another feature in Query Analyzer can analyze a query and recommend appropriate indexes. Run Index Tuning Wizard from the Query menu again. This wizard analyzes the query and gives recommendations for indexes. As shown in Figure 5, it recommends two indexes to be created ([TABLE4].[T4COL4] & [TABLE1].[T1COL5]) and also indicates performance will improve 99 percent!

Figure 5. Index Tuning Wizard. Click on thumbnail to view full-sized image.

After creating the indexes, the execution time declines from 4,125 milliseconds to 110 milliseconds, and the new execution plan shown in Figure 6 shows only two table scans (not a problem as TABLE2 and TABLE3 both have limited records).

Figure 6. Execution plan after indexes. Click on thumbnail to view full-sized image.

This was just an example of what proper tuning can achieve in terms of performance. In general, SQL Server's auto-tuning features automatically handle many tasks for you. For example, reordering WHERE clauses will never yield any benefit, as SQL Server internally handles that. Still, here are a few things you should keep in mind while writing SQL queries:

  • Keep the transactions as short as possible. The longer a transaction is open, the longer it holds the locks on the resources acquired, and every other transaction must wait.
  • Do not use DISTINCT clauses unnecessarily. If you know the rows will be unique, do not add DISTINCT in the SELECT clause.
  • When possible, avoid using SELECT *. Select only the columns from which you need the data.
  • Consider adding indexes to those columns causing full-table scans for your queries. Indexes can result in a big performance gain, as shown above, even though they consume extra disk space.
  • Avoid using too many string functions or operators in your queries. Functions like SUBSTRING, LOWER, UPPER, and LIKE result in poor performance.

Summary

In this article, I barely scratched the surface of performance tuning. Just reading this article will not make you an expert on all aspects of performance. But I hope you have gained a better appreciation for the methodology and acquired enough knowledge to be able to start the process. For optimum results, you need to tune every aspect of the application starting with the bottlenecks. Even experienced developers make small mistakes that prove costly in terms of performance. Performance tuning helps to identify these mistakes and makes your product not only more scalable, but also more reliable under stress conditions.

Rahul Kuchhal is a software architecture manager with Synygy's performance tuning group. He holds a master's degree in computer science with specialization in network communications. He has been involved in designing and developing large-scale projects in performance management, telecommunications, and banking industries for more than seven years now. These days, as the manager of a performance tuning group, he works full time on all things performance.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more