天天看點

Java Performance: the top 5 pigsJava Performance: the top 5 pigs

Java Performance: the top 5 pigsJava Performance: the top 5 pigs
Java Performance: the top 5 pigsJava Performance: the top 5 pigs
Home    Books    Boards    Newsletter    Resources    About    Contacts

Java Performance: the top 5 pigsJava Performance: the top 5 pigs
Java Performance: the top 5 pigs

(from September Bitter Java newsletter)

I recently completed a performance-tuning gig with a customer. The Internet message board had nothing to do with Java. Instead, it was in a language that I have rarely used, but all of the major problems were familiar to me. In general, the database was not optimized, the application did not connection pool and there was no caching. After we fixed those problems and removed some unnecessary database writes, the performance improved by a full order of magnitude. As a consultant, those problems are common to many of my customers. That prompted this article, which lists my top 5 Java performance pigs.

1. Round tripping.

The most common way to separate user interfaces is through the use of a well-known design pattern called Model/View/Controller. When the user interface and the model are distributed, there is significant potential for round-tripping. This anti-pattern occurs when we choose to iterate over a major system boundary. Figure 1 shows the iteration over user interface fields across a major interface boundary. The left hand side of the figure represents the model, and the right hand side represents the view. The underlying implementation does not matter. We could be using an applet to implement the view, or we could be using a multi-layer implementation with servlets or JSPs to render a dynamic view. Either way, the direct access of the model's fields that map onto user interface fields is a prime example of round tripping, and can significantly hinder optimal performance. Each field will require a round-trip communication between the client and server, which is not too expensive, until the cumulative effects are multiplied. EJB models that populate list boxes or tree components are especially troublesome.
Java Performance: the top 5 pigsJava Performance: the top 5 pigs

Distributed tools, like EJBs, make it easy to do. A common Internet topology has the web server and servlet container, or web application server, deployed in the DMZ with EJBs deployed inside the corporate firewall. With this configuration, a common mistake is to make a distinct call to the EJB layer for every field required by the user interface. This gets extraordinarily expensive. It is much better to buffer all of the calls together, and make them in one trip. Common design patterns that accomplish this technique are the façade pattern and the distributed command bean.

A good caching model can cover up a whole lot of bad implementation. The best way to hide a bad enterprise architecture is to eliminate its use for the expected case. Caching can dramatically reduce round tripping. Caching in front of enterprise data access can turn a network communication and a database access to a simple memory read and a page build. A cache for static content can be placed upstream of an entire Internet architecture, including the dispatcher, saving an amazing amount of traffic and system load. JSP fragments can be cached, saving servlet execution time. Together, these effects are cumulative. This figure is an adaptation of the figure in an article called ScalingUp e-business Applications with Caching by Mike Conner, George Copland, and Greg Flurry, at http://service2.boulder.ibm.com/devtools/news0800/art7.htm#toc6. Caching in the network, the edge servers and throughout the dynamic application saves nearly an order of magnitude:

Java Performance: the top 5 pigsJava Performance: the top 5 pigs

2. Overuse.

Object-oriented tools and models make it very easy to use complex models. Sometimes, complexity is too easy to achieve. Consider EJB development environments. In many cases, making an entire object model persistent involves just a few keystrokes. While this development style is easy to accomplish with good tools, it is difficult to make such a model perform well. A better approach is to look at the objects in a model, and see which ones make the most sense as persistent EJBs. Objects having requirements to be persistent, transactional and shared are the best candidates. Some objects may not need to be persistent at all. In other cases, tables like tax tables that rarely change can be implemented more efficiently with other means, like a stateless session bean. In other cases, special considerations may make bean managed persistence the choice over container-managed persistence. Careful, premeditated choices usually trump system-wide EJB deployments.

There are other common examples of overuse. When large teams or geographically distributed teams work together on complex problems, the number of layers can get out of control because of lack of communication. Taligent, which had many of the best object-oriented analysts in the world, may have developed a fatal case of this disease. User interfaces, which can be heavily layered, have frequently created more events than even the fastest processors could handle. Deep inheritance graphs can make code all but unreadable, and require a stiff performance tax.

3. Poor database design or integration.

This problem is definitely not unique to Java, but poor database integration has bitten this community especially hard. The ease at which modern development environments allow database access is, in some ways, alarming. I have talked to several startups with production, data driven businesses that had no DBA on the staff. It is easy to develop database models with no organizational forethought or any index structure at all. For significant deployments, a good data design, good integration between the database and application, and a strong management philosophy and execution are critical success factors. These tips can go a long way toward good integration:
  • The database model is not the same thing as an object model. Develop a strategy that will allow strong, efficient access of database data, with minimal redundancy and good performance. Also, develop a strategy for the management of mapping the object model to the database, such as the employment of a mapping layer like TopLink. If necessary, get some consulting help to build a strong data model.
  • Use Explain. Most database environments have a tool that explains the access plan for queries. The access plan is the algorithm that a database engine will use to process a query. Shorter tables may be processed quickly by loading the entire tables into memory, but watch for table scans of larger tables, which can indicate performance problems. Usually, in these cases, the use of an index is much faster.
  • Make sure that statistics are run at regular intervals, and that the database is tuned correctly. Statistics are used to help the optimizer make good decisions. Two important tuning parameters for most databases are the row buffer (which is basically a in-memory cache) and the lock list. A large row buffer will allow frequently accessed database rows to stay in memory, so that subsequent reads do not have to access the disk. A large lock list will help to avoid unnecessary lock escalation (where the database trades in many row level lock for a single table level lock, badly crippling concurrent access.)
  • Watch long units of work. They can accumulate too many locks, eat up log space, and harm concurrency.

4. Too many web objects.

Internet performance has gotten much better over time. Bandwidth has improved, especially over the last mile, or the connection between the customer and the formal internet access at the Internet service provider. Many user interface designers are using improved performance as an excuse to get sloppy. In fact, applications with a high number or very large objects can take significantly longer to load. Consider this screen shot of an application from IBM called Page Detailer:
Java Performance: the top 5 pigsJava Performance: the top 5 pigs
The figure shows the objects and total load time of amazon.com. There are forty-six total items, and it takes the whole page over five seconds to load. By contrast, consider the page detail for yahoo.com:
Java Performance: the top 5 pigsJava Performance: the top 5 pigs
Four items take a half of a second to load! Though there is dynamic content, the Yahoo page is lightning fast, and is a textbook example of simplicity and function in an interface. I should probably practice what I preach, because both the Bitter Java and J2Life sites have a good number of graphics, but neither yet has the traffic to demand simplification. In situations where the volume is low or other dynamic contents are very high, a relatively large number of graphics or very large objects might not matter, but in cases where relatively short load times are being improved, it makes sense to reduce the complexity of the user interfaces by decreasing the total number of graphics.

5. Concurrency control.

Java builds in features to control concurrent access to critical sections of code. Java concurrency control is done on the object level. An object having a synchronized method has a semaphore. When the method is entered, a thread requests the semaphore, and releases it when it exits the method. The synchronized keyword therefore means that only a single thread, per object, can access a particular method. It is not free, though. Synchronization can be overused, and fully synchronizing methods can be too heavy handed. These three tips can help to refactor overly restrictive code.
  • If only a portion of a method needs synchronization, use synchronized blocks around critical sections instead of synchronizing entire blocks. This is an example of a synchronized block of code, which creates an object for use as a semaphore:

    Object mutex;

    Subject mutex;

    synchronized(mutex)

    {

    // critical section

    }

  • Identify critical resources and code fragments, and surround only those critical sections with synchronized sections. Overuse of synchronized carries a performance penalty and also restricts concurrency.
  • In many instances, exclusive locks are too restrictive. For many applications, readers can share access to a resource, but writers need exclusive access. Using synchronized objects in this case is too restrictive, especially if reads are more common than writes. A good example is a hash table as a cache. Restricting access with the synchronized keyword blocks all readers if any one has access, which is much too restrictive. In this case, read/write locks can be used. There are many examples. This is one that I have used successfully: http://home.earthlink.net/~kohliaman/technical/rwlocks.html.

Honorable Mentions

Lazy or ignorant choice of algorithm or class. We have all used an o(n2) bubble sort where an o(n log n) shell sort would work better. Applications that require random access to a collection with no traversal should use an o(1) hash table rather than an o(n) array.

Poor memory management. We've all seen optimizations from the local wizard render clear code completely unintelligible for the big payoff of three bytes in an obscure block of error handling code. Granted, memory management is less important today. However, objects allocated for session state or custom connection frameworks are multiplied, so they are not the places to get complacent.

Applets. New customers to Java invariably try them, and the download times are still dog slow. It's generally much better from a performance and systems management standpoint to try to make a HTML user interface fly.

Summary

Java performance depends mainly on reducing round-trip communications, using resources wisely, and minding the performance of other systems like databases and user interfaces. In my experience, the two biggest performance boosters are the addition of a cache and effective control of round-tripping. If you would like to discuss these or others, please feel free to check out the message boards.

by Bruce A. Tate