HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

Building J2EE Applications, by Misha Davidson

The highly structured nature of applications built using J2EE technologies lends itself well to design patterns for performance optimization. This article examines a number of such patterns and suggests optimal ways of using them to improve latency, throughput, and overall scalability of J2EE applications.

Application Performance Defined
The standard performance metrics for computer systems are latency and throughput. In the context of J2EE applications, latency is the time that elapses between the moment a client request is received by the server and the moment a response is sent back. Throughput is the number of client requests that are handled per second.

Both latency and throughput are important. To achieve high performance, you want to minimize the latency of your application while increasing throughput. In practical terms, subsecond latency is usually sufficient for Web systems. Acceptable throughput rates vary depending on the application; however, throughput of hundreds of e-commerce (e.g., shopping cart) transactions per second is considered quite good.

Both latency and throughput define the performance of a system under a particular load. However, J2EE systems operate under rapidly changing loads, at times coping with an influx of client requests an order of magnitude greater than normal. The system's ability to cope with such an increased load is called scalability.

Typically, system performance degrades with increased client load: latency increases and throughput falls. When a poorly designed system is confronted with an unexpectedly high load (see Figure 1), its latency grows nonlinearly and becomes unacceptably high while throughput falls sharply. An ideal system maintains a constant latency regardless of the load level, while its throughput scales linearly with the load. While ideal systems don't exist, a well-designed system should scale well, maintaining low latency and high throughput under a wide range of loads. This article examines techniques for building such applications.

Figure 1
Figure 1

Structure of J2EE Applications
J2EE applications are comprised of three tiers of components - client, server, and enterprise information system (EIS) resources. The client can be a static page or an applet running in a browser (HTML or WML), or a J2EE client application running inside a J2EE client container. On the server tier, servlets and JavaServer Pages (JSPs) that run inside a Web container generate the application Web UI. EJBs that run in an EJB container encapsulate application logic and data access. A typical J2EE application uses a relational database as its EIS-tier. Figure 2 shows the relationship between J2EE components.


Figure 2

This article presents techniques that affect the performance of Java and database-backed systems in general, optimizations that apply to individual J2EE component types, and a number of J2EE application patterns that improve performance.

J2EE Application Performance Basics
At their core, J2EE applications are Java programs that utilize a database. As such, J2EE applications are subject to all performance principles that apply to traditional Java and database programming. Good coding techniques and efficient use of the database are a prerequisite for writing high-performance J2EE applications.

Database Usage Basics
Databases can be huge - thousands of tables and millions of rows of data. Improper use of a database in a J2EE application drastically affects performance. Keep the following points in mind when architecting the database portion of the application:

  • Avoid n-way joins: Every join has a multiplicative effect on the amount of work the database has to do to execute a SQL statement. Multiway joins degrade the performance of an application once the data set becomes sufficiently large. You may not notice the performance hit on a development system using a sample database, but it'll manifest itself in production. Make sure any multiway joins your application is using are really needed.
  • Avoid bringing back thousands of rows of data: If you don't, your server will spend an inordinate amount of resources and time storing them in memory. The user whose actions caused the query will get a sluggish response, and everyone else's ability to do work will be impaired. If you absolutely have to bring back such a large data set, consider bringing back just the primary keys first and fetching individual rows on-demand. This approach doesn't reduce the total amount of work, but, it spreads it out, greatly reducing the immediate response time.
  • Cache data when reuse is likely: Accessing a database is by far the most expensive operation an application server can perform. Thus, the number of such accesses should be minimized. In particular, if an application frequently refers to a certain subset of values from the database, those values should be cached.

Java Coding Guidelines
Coding techniques affect the performance of J2EE applications, though not as much as database usage. Here are a few basic points to keep in mind when writing J2EE applications:

  • Avoid unnecessary object creation: Current implementations of JVMs are much more efficient in managing object creation. Still, it's one of the more expensive operations that JVMs perform.
  • Minimize the use of synchronization: Synchronization blocks reduce scalability of applications by forcing serialization of all concurrent threads of execution. Setting up synchronization also requires a nontrivial amount of resources from the JVM. Therefore, serialized blocks should be kept small and used only when absolutely needed. The same rule applies to using standard Java classes. The JDK usually provides at least two implementations for each data structure, one with synchronization and one without (e.g., Vector is synchronized and ListArray is not). When using JDK classes, pick synchronized implementations only when synchronization is actually needed; use unsynchronized versions in all other cases.
J2EE Component Performance
Now that we've defined the basics of writing performant J2EE applications, let's look at specific performance techniques for writing servlets, JSPs, and EJBs.

Servlet Performance Hints
The first thing to note about writing servlets for performance is that the use of a SingleThreadModel interface should be avoided. SingleThreadModel is a marker interface that tells a Web container that the code inside the service() method of the servlet class is not thread-safe. Typically, the container will create and possibly cache multiple instances of the servlet class to handle the concurrent invocations of the service() method. Since multiple instances of the servlet class can be used at the same time, marking a servlet with a SingleThreadModel doesn't guarantee thread-safety of an application. Multiple instances of the servlet class may still be accessing the same underlying classes and data at the same time. Managing multiple instances of the servlet does, however, incur significant overhead for the Web container. Therefore, instead of writing bad servlet code and covering it up with a SingleThreadModel, it's preferable to write thread-safe code to begin with.

In some cases, it may be possible to speed up the writing of the output from a servlet by using an OutputStream instead of a PrintWriter (both can be obtained from the ServletResponse). In general, a PrintWriter should be used to ensure that all character set conversions are done correctly. However, if you know that your servlet returns only binary or ASCII data, you can use an OutputStream instead. An OutputStream writes data directly to the wire, thus forgoing the character set conversion overhead.

Using the getRemoteHost()method on a ServletRequest can dramatically increase the latency of your application. This call performs a remote DNS look-up on the IP address of the client. This operation typically takes seconds to complete and should be avoided at all costs.

JSP Performance Hints
Even though JSPs provide an elaborate functionality built on top of servlets, the performance of JSPs is roughly comparable to that of servlets. JSPs compile to servlets; compilation introduces a trivial amount of initialization code. Compilation happens only once and JSPs can be precompiled, eliminating any runtime overhead. JSPs are slower than servlets only when returning binary data, since JSPs always use a PrintWriter, whereas servlets can take advantage of a faster OutputStream.

JSP custom tag libraries can encapsulate arbitrarily complex Java functionality in a form that can be easily used by Web designers who aren't that knowledgeable about Java. Thus, JSP custom tag libraries are an effective tool for dividing the work between programmers and Web designers. However, every time a custom tag is used on a page, the Web container has to create a new TagHandler object or fetch it from the tag cache. In either case, excessive use of custom tags may create unnecessary processing overhead.

BodyTags are a special kind of custom tag. They have access to, and can do transformations on, the contents of their bodies. The use of BodyTags causes the contents of the page between the start and the end portions of the tag to be copied in memory. They can be nested and iterate over their bodies. Using multiple levels of BodyTags combined with iteration will likely slow down the processing of the page significantly.

EJB Performance Hints
Conversational session beans that write to the database should be designed to prevent conflicts when doing the update. The simplest way of achieving transactionality in this case is to lock the whole table, or just the rows that are being updated. However, this approach incurs high overhead and greatly reduces the scalability of EJBs written this way. Instead, optimistic concurrency control may be used, so there would be no need to lock the data that's being updated. Instead, all the updates to the database receive an additional WHERE clause containing the old values of all columns in the row being updated. In case of contention, the first writer succeeds while everyone else fails, because the WHERE clause doesn't match after the first update. The result is the same as when strict locking is used. However, optimistic locking involves smaller overhead and allows multiple updates to proceed in parallel, thus increasing the scalability of the system. Note that, as with most database operations, the benefits of optimistic locking degrade when there are many collisions and the application spends a lot of time dealing with failed updates.

Entity EJBs may form a "graph" of related beans. Not all parts of such an object graph may be needed by an application at all times. To reduce the overhead of loading such object graphs, a technique of "lazy-loading" dependent EJBs can be used.

For example, suppose we have an insurance policy bean that refers to holder and vehicle beans, and we're interested only in the identification of the vehicle in that policy, not the identity of the policyholder. In this case, loading the holder bean at the same time the policy bean is loaded would be wasteful. Instead, we load the dependent beans only when they're actually needed (see Listing 1).

Some application servers use optimistic locking and lazy loading internally in their container-managed persistence (CMP) implementations.

J2EE Application Performance
Good coding practices and efficient J2EE components are only a part of writing high-performance J2EE applications. Architecture has a critical effect on application performance. Here are some examples.

Read-Only Web Applications
Many real-world J2EE applications such as on-line reservation systems and catalogs provide read-only access to a large set of data. These applications handle a large number of queries from different users; some of the query results are large, but all are short-lived. Using entity EJBs is overkill for this type of application, as they individually read each row of data and the server expects to keep that data in memory for a while. Instead, this type of read-only Web application lends itself well to the following optimization. Embed SQL statements that access the data in JavaBeans. Use these beans directly from JSPs to fetch the data.

Going from JSPs to the database without involving the EJB layer significantly speeds up the application. It also reduces the overall complexity. This shortcut is acceptable because data access is read-only and there's little processing logic. This approach isn't suitable for applications that require complex processing or write to the database.

Infrequently Changing Shared Data
On the other end of the application spectrum are applications that share a small subset of infrequently changing data. A typical example of such application functionality is a list of top stories or most popular items on a shopping site. Stateless session EJBs can be used to efficiently cache and manage such data.

There are three types of objects involved in this scenario: stateless session EJBs that access and store the data; clients, for example, JSPs that use this data; and a special remote Cursor class that's owned by the client and used to keep track of which rows were read by that client. To cache the data, the session EJB in its ejbCreate() method performs a SQL query, reads in the result, and stores the individual rows as elements in a ListArray. To read a row of data, clients execute a business method, readNextRow(), on that session EJB. However, different clients need to access different rows stored by that session bean. To do that, clients pass in a special cursor object that holds the number of the last row read by that client. The session bean uses that number to find the next row to give to the client. It then increments the row counter inside the cursor before returning the row (see Listing 2). A more efficient implementation results if the client manages the row number inside the cursor.

To prevent the data from going stale, readNextRow() must implement a refresh mechanism. In the above example, the data is refreshed after it's been accessed a certain number of times. Alternatively, data can be refreshed after it's been cached for a certain duration.

Minimizing the Number of Network Round-Trips for J2EE Clients
J2EE client applications may call EJBs directly. Each call involves a JNDI look-up and a network round-trip, both of which can be expensive. When a J2EE client contains all the processing logic, it has to perform multiple JNDI look-ups and network round-trips to access the data presented by entity EJBs on the server. This overhead can be reduced to a single look-up and round-trip if the processing logic is encapsulated in a single session bean. That bean, in turn, encapsulates all the entity beans that would otherwise be accessed directly by the J2EE client. In this case, a J2EE client acts as a rendering engine for the results contained in a single object returned by the session bean.

Deployment Strategies
Application design and coding techniques determine the performance of the resulting systems. The way in which an application is deployed has an effect as well.

  • Minimize interprocess communication: If possible, run the Web server and the application server in the same process. Separating the two can significantly increase the response time of your application.
  • Use clustering to increase scalability: As long as your application doesn't require the servers in your cluster to communicate with each other, adding more servers keeps latency low while increasing throughput.
  • Provide at least the minimal set of resources needed to run an application. This includes setting a sufficient size for the JVM's heap, providing a reasonable number of database connections, and setting a reasonable size for thread, servlet, and bean pools. Failure to provide enough resources results in contention and severely degrades application performance. However, unreasonably high resource settings will degrade performance as well. For example, if the heap size is greater than the physical memory available, the JVM will thrash instead of doing useful work. Similarly, if the size of the database connection pool is greater than the number of connections allowed, the application will spend a lot of time dealing with what it perceives as database failures (see Figure 3).


Figure 3

Summary
J2EE applications, like any application, should be architected for performance and scalability. J2EE application designers can take advantage of the following common principles.

  • Entity beans are too heavy to use with read-only data, use JDBC instead.
  • Stateless session EJBs can be used to efficiently represent infrequently changing shared data.
  • Minimize the network overhead for J2EE clients by locating the processing logic in a session bean on the server.
In building individual J2EE components, the following rules apply. For servlets:
  • Avoid using a SingleThreadModel interface
  • Use print OutputStream instead of PrintWriter to output binary/ASCII data
  • Don't use a getRemoteHost() method on a ServletRequest
For JSPs:
  • Be careful when using nested BodyTags
For EJBs:
  • Use optimistic locking to improve performance when doing database updates
  • Use lazy-loading to efficiently handle dependent beans
Because J2EE applications are Java programs that use databases, common programming principles are applicable to J2EE apps as well. Specifically, for JDBC access:
  • Avoid n-way joins
  • Avoid bringing back thousands of rows of data
  • Cache data when reuse is likely
For Java programming:
  • Avoid unnecessary object creation
  • Minimize the use of synchronization
Finally, when deploying J2EE applications, provide them with sufficient resources and use clustering to achieve higher scalability.

References

  1. J2EE Blueprints: http://java.sun.com/j2ee/blueprints/
  2. Larman, C., and Guthrie, R. (2000). Java 2 Performance and Idiom Guide. Prentice Hall.
  3. Professional Java Server Programming J2EE Edition. (2000). Wrox Press.
  4. Roman, E. (1999). Mastering Enterprise JavaBeans and the Java 2 Platform, Enterprise Edition. Wiley.
  5. Halter, S., and Halter, S.M. (2001). Enterprise Java Performance. Prentice Hall.
Author Bio Misha Davidson is a product manager for SilverStream Software, Inc.
[email protected]

	


Listing 1

public class PolicyBean implements javax.ejb.EntityBean {
  public Vehicle vehicle; // data we're interested in
  public Holder holder;


  public void ejbLoad() {} // Load nothing by default


  public String getVehicleId() {
    loadVehicle(); // insure that dependent beans are loaded
    return vehicle.getVIN();
  }


  private void loadVehicle() { // do this in-line for better performance
    if (vehicle != null) {
      // get initCtxt hereŠ
      home = (VehicleHome) initCtxt.lookup("VehicleHome");
      vehicle = home.findByPolicy (policyID);
    }
  }
}




Listing 2

public class SharedData implements SessionBean {
  private ListArray m_data; // cached data
  private int m_accessCnt; // access counter
  public void ejbCreate() {
    reloadData(); // get data for the first time
  }
  private void reloadData() {
    // do sql query, store result in m_data
  }
  public String[] readNextRow (Cursor curs) {
    if (m_accessCnt++ > 1000) { // refresh?
      m_accessCnt = 0;
      reloadData(); // re-get the data
    }
    int i = curs.incRow(); // update row
    return (String[]) m_data.elementAt(i);
  }
}


  
 
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.