Oracle9i Application Server Best Practices Release 1 (v1.0.2.2) Part Number A95201-01 |
|
This chapter provides a checklist of what we consider performance and scalability best practices in designing, implementing, tuning, and testing your Web application that is to be deployed on Oracle9iAS Release 1 (v1.0.2.2). It is based on the collective experience and work of the PDC performance group. Some of the checklist items are based on results of our performance tests on Oracle9iAS. Others are based on common performance issues we encountered while working with our customers who are building and deploying their Web application on Oracle9iAS.
This chapter contains these topics:
Some developers suggest you should make it work, then make it fast. This is common advice by those who think one should avoid any premature optimization that would yield little benefit. It really should not be taken literally that performance and scalability should be deferred until the application is in production.
We have had cases of customers who were experiencing performance problems days before their system was ready to go live. Some common complaints were that their system was unable to support even a small number of concurrent users, their response time was unacceptable, or there was a memory leak in the system which degraded performance. On some occasions, we were able to help them by making some configuration suggestions. Other times, we found the problems were so ingrained in the application's architecture that there were no simple solutions we could give them other than suggesting that they redesign the system.
This chapter focuses on applications written in Java using servlets and JavaServer Pages (JSPs). It includes sections on performance and tuning tips for servlets and JSPs deployed on JServ, Java performance tips, configuration tips for the Oracle HTTP Server, and some suggestions on performance testing.
Some of the performance and tuning tips for servlets, JSPs, and the Oracle HTTP Server are from the Oracle HTTP Server Performance Guide, available in the Oracle9i Application Server Platform-specific Documentation. We include examples as well as results of our performance tests on these practices.
This chapter is intended for application designers and system administrators who are already familiar with Java, servlets, JSPs, and Apache.
It is critical to consider performance when designing your Web application, but it is also important to remember that there are tradeoffs between performance, availability, security, and flexibility. For more information on availability and security, you can refer to Chapter 1, "Availability of Java Applications" and Chapter 4, "HTTP Security". Here, the focus is the tradeoff between performance and flexibility.
For example, in Java you can define a class as final to improve performance. However, a final class cannot be subclassed; so you will lose some flexibility. Another example is the use of an object factory to centralize object creation instead of creating the object directly. The object factory will undoubtedly add performance overhead to the application, because of the extra method calls, but it allows you more flexibility.
Balancing these tradeoffs is not always easy; often it is the subject of debate. Also, a performance gain in one area can be a performance loss in another. Our suggestions are:
Servlets and JavaServer Pages (JSPs) are used on the server side to access data and to generate dynamic content in a Web application. Their framework makes it possible to build Web applications quickly and easily. The disadvantage is that their flexibility can make debugging very difficult. JSPs can be especially difficult, because you can easily mix HTML with Java code in the same page.
The architecture does matter when you design your Web application. If it has a fundamental problem, then there is usually very little tuning you can do to improve performance significantly without redesigning the system.
We use the following customer application as a case study. The application used JSPs. The user logged in through a home page and was authenticated. If the authentication succeeded, then the user's profile was retrieved from the database, and the home page forwarded the user to the appropriate menu page. This is a common flow of operation in many Web applications.
The following is a high level description of the original architecture:
beginRequest()
method to check if the user has been authenticated based on the cookie information from the user.
endRequest()
method to remove the entry from the Hashtable and release the profile object and resources. The user's profile or any state information is encoded in a cookie and returned to the user, so subsequent requests from the same user can parse the cookie for the user information. No state information is kept in the server using HttpSession or session scope beans.
This architecture has several design and Java performance problems:
User information in the cookie has to be parsed, and the user has to be reauthenticated in each request.
The application used this architecture to appear stateless. User information is encoded in a cookie and sent back to the user at the end of each request. Each JSP calls the beginRequest()
method to parse the cookie and reconstruct the user profile object from the cookie before processing each request.
Problems:
High-cost Hashtables are used to store objects that will never be accessed concurrently.
The implementation in the beginRequest()
stores the user and request-related information in static Hashtables using the Java thread ID as the key. The objects are stored in the Hashtables at the beginning of the request and removed at the end of the request. Any operations that need to use the resources or access the user data use the Java thread ID to retrieve these objects while processing the request.
Problems:
put()
, get()
, and remove()
are constantly active in these Hashtables, because each element will remain in them for only a short time.
The endRequest()
must be called manually to release the resources before exiting the JSPs. Problems:
endRequest()
to remove the objects and free the resources from the Hashtables can result in a major memory leak.
The JSP developer must understand the flow of operations between the JSPs and decide which JSP should make the endRequest()
call.
endRequest()
method was not called accordingly.
Therefore, many of the connections could not be reused, and objects were not released properly, causing gradual memory leaks.
We are not suggesting that making an application stateless is a bad practice. Stateless applications are more highly available and secure, but you pay a price in performance.
We rewrote part of the application by using many of the best practices suggestions in this chapter, and we were able to improve performance without giving up any functionality. But the changes we made were not trivial. In order to get a significant performance gain, we needed to redesign part of the architecture and rewrite much of the Java and JSP code.
We use this example to stress the importance of selecting the right architecture before implementing your application. If you wait until the system is in production before you uncover these types of performance problems, then it may be too late to fix them. It will certainly cost more than getting it right the first time.
This section contains our design and implementation recommendations for using servlets and JSPs. Because the JSP is an extension of the servlet, some of these best practices are applicable to both.
Oracle9iAS supports servlets and JSPs through Oracle JavaServer Pages (OJSP) and JServ. The performance tips in this section are specific to using JServ and OJSP through the Oracle HTTP Server.
Use session to store state information that need not be persistent but can be recovered easily.
A real-world Web application can rarely be totally stateless. Most Web applications have to service a sequence of requests from the same user with certain state information passed between requests. When a Web application is called stateless, it usually means the application takes the extra effort to ensure the state information is either persistent or recoverable in the event of a process failure on the server side. Though it would be nice to make a Web application truly stateless to improve availability, it is generally costly to do so.
The servlet and the JSP runtimes support session tracking. State information can be saved in the session objects associated with a session ID. As long as the session has not expired, a client who comes back to the same JServ process with the session ID can retrieve any data that is stored in the HttpSession object. The data stored in the session will be released either when the application explicitly calls invalidateSession()
or when the session times out based on the session timeout parameter.
The most commonly cited drawbacks with using sessions are:
Our performance tests have shown, however, that using sessions for certain types of data can minimize these problems and provide significant performance gains.
In our case study, we found the application's user profile information can take advantage of using sessions because:
We modified the application in the case study by using a session scope bean to store the user profile information when the user logged in from the home page. This eliminated some of its performance problems:
If the session was still valid when the user returned with the next request, then we skipped the step of parsing the cookie or reauthenticating, unless the request required update or access to restricted data. Only in the case of session failure did we have to parse the cookie to recreate the user profile object.
endRequest()
method call in the JSP.
Because we used a session scope bean, we called invalidateSession()
explicitly to end the session if the user logged out. Otherwise, we simply let the session time out.
After implementing these changes and others we suggest in this chapter, we ran a performance test comparing the original and modified versions, using exactly the same configuration. The test simulated a set of seven requests per user. Each user logged in, selected some functions from the menu, and logged out. The test was run with 100 concurrent users who repeatedly executed these seven requests for a duration of one hour. Our modified version ran about eight times faster than the original application , as shown in Figure 2-1.
The memory overhead for keeping the user profile in session in our modified version was only about 1MB for 12,000 sessions. This is a relatively small overhead compared to the significant performance gain.
We suggest the following guidelines when using sessions:
Objects that are stored in the session objects will not be released until the session times out. If you hold any shared resources that have to be explicitly released to the pool before they can be reused (such as a JDBC connection), then these resources may never be returned to the pool properly and can never be reused.
Make sure there is sufficient memory for the number of sessions created before the sessions time out.
You can use any platform-specific utilities (such as ps top on Solaris) to monitor the memory usage of a JServ process. Dynamic Monitoring Service (DMS) also provides additional built-in metrics that you can use to display the memory usage of the JVM in each JServ process.
Use JDBC connection pooling and connection caching.
Constant creation and destruction of resource objects can be very expensive in Java. In "Java Performance", we suggest using a resources pool to share resources that are expensive to create. The JDBC connections are one of the most common resources used in any Web application that requires database access. They are also very expensive to create. We have seen overhead from hundreds of milliseconds to seconds (depending on the load) in establishing a JDBC connection on a mid-size system with 4 CPUs and 2 GB memory.
In JDBC 2.0, a connection-pooling API allows physical connections to be reused. A pooled connection represents a physical connection which can be reused by multiple logical connections. When a JDBC client obtains a connection through a pooled connection, it receives a logical connection. When the client closes the logical connection, the pooled connection does not close the physical connection. It simply frees up resources, clears the state, and closes any statement objects associated with the instance before the instance is given to the next client. The physical connection is released only when the pooled connection object is closed directly.
The term pooling is extremely confusing and misleading in this context. It does not mean there is a pool of connections. There is just one physical connection, which can be serially reused. It is still up to the application designer to manage this pooled connection to make sure it is used by one client at a time.
To address this management challenge, Oracle's extension to JDBC 2.0 also includes connection caching, which helps manage a set of pooled connections. It allows each connection cache instance to be associated with a number of pooled connections, all of which represent physical connection to the same database and schema. You can use one of Oracle's JDBC connection caching schemes (dynamic, fixed with no wait, or fixed wait) to determine how you want to manage the pooled connections, or you can use the connection caching APIs to implement your own caching mechanisms.
See Also:
|
Use JDBC statement caching.
Use JDBC statement caching to cache a JDBC PreparedStatement
or OracleCallableStatement
that is used repeatedly in the application to:
The performance gain will depend on the complexity of the statement and how often the statement has to be executed. Since each physical connection has its own statement cache, the advantage of using statement caching with a pool of physical connections may vary. That is, if you execute a statement in a first connection from a pool of physical connections, then it will be cached with that connection. If you later get a different physical connection and want to execute the same statement, then the cache does you no good.
See Also:
|
Use mid-tier caching mechanisms Oracle9iAS Web Cache and Oracle9iAS Database Cache to cache commonly shared or reused data.
Caching commonly shared data in the mid-tier can provide significant performance improvement. Oracle9iAS Web Cache and Oracle9iAS Database Cache can be used in the mid-tier to cache HTML pages, large shared objects, or data.
Oracle9iAS Web Cache can store both static and dynamically generated Web content. Similarly, Oracle9iAS Database Cache can store frequently used data and stored procedures. By storing frequently accessed pages, data, or procedures in memory, you no longer need to go through the Oracle HTTP Server, and you get significant performance improvements.
Oracle9iAS Web Cache can be added as a front end to the Oracle HTTP Server during deployment and does not require any application code changes. However, you may be able to get additional benefit if you design your application to take advantage of its particular features and functions.
These components have very different performance implications depending on how they are deployed and configured.
Avoid using more than one database connection simultaneously in the same request.
Using more than one database connection simultaneously in a request can cause a deadlock in the database. This is most common in JSPs. First, a JSP will get a database connection to do some data accessing. But then, before the JSP commits the transaction and releases the connection, it invokes a bean which gets its own connection for its database operations. If these operations are in conflict, then they can result in a deadlock.
Furthermore, you cannot easily roll back any related operations in case of failure, if they are done by two separate database connections.
Unless your transaction spans multiple requests or requires some complex distributed transaction support, you should try to use just one connection at a time to process the request.
Tune the database and SQL statements.
Current Web applications are still very database-centric. From 60% to 90% of the execution time on a Web application can be spent in accessing the database. No amount of tuning on the mid-tier can give significant performance improvement if the database machine is saturated or the SQL statements are inefficient.
Monitor frequently executed SQL statements. Consider alternative SQL syntax, use PL/SQL or bind variables, pre-fetch rows, and cache rowsets from the database to improve your SQL statements and database operations.
Avoid common errors that can result in memory leaks.
In Java, memory bugs often appear as performance problems, because memory leaks usually cause performance degradation. Because Java manages the memory automatically, developers do not control when and how garbage is collected. To avoid memory leaks, your applications must:
Release failures here are usually in error conditions. Use a finally block to make sure these objects are released appropriately.
The original application in the case study was typical of this type of memory leak. The profile object and other resource objects were stored in static Hashtables, to be released at the end of a JSP by calling the endRequest()
method. But error conditions in any of the nested JSPs or forwarded JSPs would leave these objects in the Hashtables, causing memory leaks. This design is too error prone, because each JSP designer must understand the flow of operation between JSPs and know whether the endRequest()
has to be called in the page.
One application we helped review was appending error messages to a Vector defined in a serially reusable object. The application never cleaned the Vector before it was given to the next user. As the object was reused over and over again, error messages accumulated, causing a memory leak that was difficult to track down.
Avoid spawning threads in the application.
With the infrastructure provided by the servlet engine, a Web application rarely needs to spawn its own threads. Improper management of user-spawned threads can create many runaway threads, causing system deadlocks as well as memory leaks.
We saw one case in which the application was spawning a thread each time it wrote to a user-defined log file, so the client did not have to wait for the expensive log operation to complete before receiving the response. Unfortunately, the implementation failed to handle some exception conditions. When these exception conditions were encountered, it created thousands of runaway threads to retry the log operation, causing huge memory leaks and performance degradation.
Consider using SingleThreadModel in servlets as a resource pool and to minimize synchronization if appropriate.
The SingleThreadModel implementation in the servlet engine is similar to maintaining a pool of serially reusable servlet instances, where each servlet instance can be used by only one thread at a time. Therefore, any servlet which implements the SingleThreadModel is considered to be thread-safe, and no synchronization is needed when accessing any servlet instance variables or objects.
This is effectively getting the support of a managed pool without the overhead of implementing it yourself. In one of our performance tests, we have seen performance improvement of up to 25% under load by using SingleThreadModel to reduce synchronization and manage reusable objects.
If your application uses a resource that is expensive to create, and you would like to be able to reuse it serially, then you can implement your application by using a SingleThreadModel in a servlet. You can create the resource in the servlet's init()
method and save it as an instance object. Since the SingleThreadModel is thread-safe, the resource object can be used by only one thread at a time. You can then release the object in the servlet's destroy()
method when the instance is to be shut down.
The main difference between using SingleThreadModel to pool resources and implementing your own pool is flexibility. With SingleThreadModel, the number of objects you can create is controlled by the maximum number of instances per process multiplied by the maximum number of JServ processes.
For example, if you use SingleThreadModel with 10 instances per JServ process and there are 10 JServ processes, you can end up creating 100 instances of a resource. These resources are usually not released until the instance's destroy()
method is called. So SingleThreadModel is not as flexible as implementing your own pool, but it is simple to use and can provide good performance gains for the appropriate applications.
Examples where SingleThreadModel is not appropriate:
Resources would be wasted when the system is idle.
Consider using SingleThreadModel if:
If you are using SingleThreadModel:
Perform any costly one-time startup operations in the servlet init() method.
Use the servlet's init()
method to perform any costly one-time initialization. Examples include:
Separate presentation logic from business and data access logic in JSPs.
Because it is possible to include any Java code in the JSPs, it is easy to mix business and data access Java code with the presentation logic in the JSPs. Including business and data access logic in the JSPs makes the application much more difficult to maintain and debug. It increases the footprint of the JSPs and limits the efficiency of resource use and management.
For example, if each JSP has its own Java code to handle database connections and data access, it will be much more difficult to allow the database connections to be reused or shared when processing a request and between requests. Because a database connection is an expensive resource, sharing failures can have significant performance impacts.
Choose static or dynamic include appropriately.
The JSP has two different include mechanisms:
<%@ include file="filename.jsp" %>
<jsp:include page="filename.jsp" flush="true" />
Static include creates a copy of the file in the JSP. Therefore, it increases the page size of the JSP, but it avoids additional trips to the request dispatcher.
If you use static includes, keep files below the 64K limit of the service method of the generated page implementation class. If you need to work around this limitation, see the section "Workarounds for Large Static Content in JSP Pages" in Oracle JavaServer Pages Developer's Guide and Reference at http://otn.oracle.com/docs/index.htm
.
Also, even if you set developer_mode=true
in the zone.properties
file, changes made to a static include file in a running system may not be reflected until you update the main JSP or delete the class file generated for the main page.
Dynamic include is analogous to a function call. Therefore, it does not increase the page size of the JSP. But it does increase the processing overhead, because it must go through the request dispatcher.
Dynamic includes are useful if you cannot determine which page to include until after the main page has been requested. Note that a page that can be dynamically included must be an independent entity, which can be translated and executed on its own.
Do not use isThreadSafe="false" if you want multithread support when using OracleJSP. Make your JSPs thread-safe if possible.
If your JSPs are not thread-safe, then you can use the JSP directive isThreadSafe="false"
so that they can be translated into servlets which implement SingleThreadModel. However, the current version of OracleJSP does not implement an instance pool (though this may change in future releases). At the current time, OracleJSP guarantees thread-safety by serializing requests to the same page if isThreadSafe ="false"
is set for the page. This can have a significant impact on performance.
We ran a simple JSP test, which uses a Java bean to query the database and return a small set of data. With 100 concurrent users, we found the performance difference of using isThreadSafe="false"
is over 50%. This number can be higher if the request has to perform more complex operations.
You should make your JSPs thread-safe and avoid using isThreadSafe="false"
if possible. If you are using a JSP as a front end to servlets in your application, then you should also make sure your servlets are thread-safe and avoid using SingleThreadModel in your servlets.
One of the ways to make your JSPs thread-safe is proper choice of variables. You can declare variables in JSPs two ways:
<%! long startTime=0; %>
<% long startTime=0; %>
Member variables are declared at the class level in the page implementation class during the translation. Therefore, if you use member variables in your JSP, the JSP will not be thread-safe unless these member variables are themselves thread-safe.
Method variables are local to the service method of the page implementation class. Therefore, they are thread-safe. Try to use method variables and make your JSPs thread-safe if possible. For more information on the differences between member variables and method variables, see Oracle JavaServer Pages Developer's Guide and Reference at http://otn.oracle.com/docs/index.htm
Set session="false" if you do not use sessions in JSPs.
The default for JSP is session="true"
. If your JSPs do not use any session, you can set session="false"
to eliminate the overhead of creating and releasing these internal sessions created by the JSP runtime:
<%@page session="false" %>
Use forward instead of redirect if possible.
You can pass control from one page to another by using forward or redirect, but forward is always faster. When you use forward, the forward page is invoked internally by the JSP runtime, which continues to process the request. The browser is totally unaware that such an action has taken place. When you use redirect, the browser actually has to make a new request to the redirect page. The URL shown in the browser will be changed to the URL of the redirect page in a redirect, but it will stay the same in a forward.
Redirect is always slower than forward. In addition, all request scope objects will be unavailable to the redirect page, because it constitutes a new request. Use redirect only if you want the URL to reflect the actual page that is being executed, in case the user wants to reload the page.
Disable JSP buffer if you are not using it.
In order to allow part of the body to be produced before the response headers are set, JSPs store the response body in a buffer. When the buffer is full or at the end of the page, the JSP runtime will send all headers that have been set, followed by any buffered body content. This buffer is also required if the page uses dynamic contentType settings, forwards, or error pages.
The default size of a JSP buffer is 8 KB. If you need to increase the buffer size, for example to 20KB, you can use the following JSP attribute and directive:
<%@page buffer="20kb" %>
If you are not using any JSP features that require buffering, you can disable it to improve performance. Memory will not be used in creating the buffer, and output can go directly to the browser. You can disable buffering by:
<%@page buffer="none" %>
This section contains our deployment recommendations for using servlets and JSPs. Because the JSP is an extension of the servlet, some of these best practices are applicable to both.
Oracle9iAS supports servlets and JSPs through Oracle JavaServer Pages (OJSP) and JServ. The performance tips in this section are specific to using JServ and OJSP through the Oracle HTTP Server.
Configure multiple JServs to improve performance and scalability.
The default configuration of Apache and JServ is to start one JServ process automatically upon start up of the Apache server, so only one JServ process is created for each host at a time. If the JServ process dies because of a JVM error, such as running out of memory, a new one will be started automatically by the monitoring mechanism in mod_jserv.
You can change the configuration to start more than one JServ process manually, so requests will be routed to the additional processes to balance the load. If the request has a session ID, it will be routed back to the same process if the session is valid. However, the drawback of manual mode is that you must restart the JServ process in case of process failure.
Our performance tests have found that in general, it is better to have more JServ processes with fewer threads than to have just one JServ with many threads. Since most Web applications have some level of synchronization in their implementation, it is better if the requests are spread out among different processes.
However, using more processes means using more memory because of the fixed memory overhead of the JVM. Also, each JServ process has to duplicate any commonly shared data in its own JVM.
Nevertheless, we found the performance gains still outweigh the memory usage in most cases. Furthermore, having more than one JServ improves availability in case of process failure.
The amount of performance gains and the number of JServ processes you should use depend on your system configuration and application. Our general recommendation is to configure two JServ processes per CPU (if you have sufficient memory) and limit the maximum number of connections per JServ to between 10 and 20.
Oracle9iAS provides scripts and information on starting and shutting down JServ processes in manual mode. Check the Oracle9iAS release notes or documentation for more information.
Set the Java initial and maximum heap size to meet your application needs.
Java performance degrades when it runs out of available heap. We found it is best not to let Java's free memory go below 25% at any time.
The Oracle9iAS default initial heap size for JServ is set to 64MB, and the maximum heap size is 128MB. This may not be sufficient for your application. You should determine your application's memory requirement for the duration of the JServ's process up time and set these values accordingly.
You can use any platform-specific utilities (such as ps, top on Solaris) to monitor the memory usage of a JServ process. Also, Dynamic Monitoring Service (DMS) provides additional built-in metrics that you can use to display the memory usage of the JVM in each JServ process. See "Dynamic Monitoring Service (DMS)" for more information.
Set autoreload.classes=false in a production system.
The default value of the autoreload.classes in the JServ's zone.properties
file is set to true
. This means that each time one of that zone's servlets is requested, every class that has been loaded from a repository in that zone is checked to see if it has been modified and has to be reloaded. This feature is usually more useful in a development environment than in a production system. You should set autoreload.classes=false
if you do not need this option.
Preload servlets at startup time.
Use the servlets.startup
parameter in the JServ zone.properties
file to include any servlets that you want to be loaded upon startup of the JServ process. This will cause each servlet's init()
method in the servlets.startup
list to be executed even before the first request is routed to the process.
Pre-translate JSPs before deployment.
You can use Oracle's ojspc
tool to pre-translate JSPs and avoid the translation overhead that otherwise has to be incurred when they are first executed. You can pre-translate JSPs on the production system or before you deploy them. Also, pre-translating JSPs allows you the option to deploy only the translated and compiled class files, if you choose not to expose the JSP source files.
See Also:
Oracle JavaServer Pages Developer's Guide and Reference at |
Turn off debug flag and developer_mode in the production system.
The debug
and developer_mode
parameters in the OracleJSP configuration can have a significant effect on performance. The debug
flag is used to display debug information. The developer_mode
flag is used to inform the JSP runtime whether it should automatically recompile and reload any JSPs that have changed since they were loaded. These parameters are useful mostly during development.
In a test using JDK 1.2 with 50 users, 128 MB heap, and the default TCP settings, the performance gains with developer_mode
off were 14% in throughput and 28% in average response time.
Since the default for developer_mode
is set to true
, you should set it to false
in the production system if you do not need this option. You can set debug
and developer_mode
to false
in a production system in the zone.properties
file as follows:
servlet.oracle.jsp.JspServlet.initArgs=debug=false,developer_mode=false
Use DMS to monitor the performance of the JServ processes.
See "Dynamic Monitoring Service (DMS)" for more information.
The "References and Resources" section at the end of this chapter lists many books and articles on the topic of Java performance. We do not intend to duplicate all that information in this section; instead we focus on those tips which have the greatest performance impact. The best practices in this section are based on common performance problems that we frequently see in our customers' applications.
Minimize synchronization.
Many performance studies have shown a high performance cost in using synchronization in Java. Improper synchronization can also cause a deadlock, which can result in complete loss of service because the system usually has to be shut down and restarted.
But performance overhead cost is not sufficient reason to avoid synchronization completely. Failing to make sure your application is thread-safe in a multithreaded environment can cause data corruption, which can be much worse than losing performance. These are some practices that you can consider to minimize the overhead:
If only certain operations in the method must be synchronized, use a synchronized block with a mutex instead of synchronizing the entire method.
private Object mutex = new Object(); ... private void doSomething() { // do things that do not have to be synchronized ... synchronized (mutex) { ... } ... }
Every Java object has a single lock associated with it. If unrelated operations within the class are forced to share the same lock, then they have to wait for the lock and must be executed one at a time. In this case, define a different mutex for each unrelated operation that requires synchronization.
public class myClass { private static myObject1 myObj1; private static mutex1 = new Object(); private static myObject2 myObj2; private static mutex2 = new Object(); ... public static void updateObject1() { synchronized(mutex1) { // update myObj1 ... } } public static void updateObject2() { synchronized(mutex2) { // update myObj2 ... } } ... }
Also, do not use the same lock to restrict access to objects that will never be shared by multiple threads. In the case study, for example, the original application used Hashtables to store objects that would never be accessed concurrently, causing unnecessary synchronization overhead.
Similarly, if the objects are always manipulated together, make sure they share the same lock.
public class myClass { private static mySpecialObject myObj; ... public static mySpecialObject getSpecialObject() { if (myObj == null) createSpecialObject(); return myObj; } private static synchronized void createSpecialObject() { if (myObj != null) return; //perform complex initialization myObj = new mySpecialObject(); ... } ... }
Making fields private protects them from unsynchronized access. Controlling their access means these fields need to be synchronized only in the class's critical sections when they are being modified.
Provide a thread-safe wrapper on objects that are not thread-safe. This is the approach used by the collection interfaces in JDK 1.2. See "PERF-25: Hashtable and Vector" in this section for more information on the use of this approach to minimize synchronization.
An immutable object is one whose state cannot be changed once it is created. Since there is no method that can change the state of any of the object's instance variables once the object is created, there is no need to synchronize on any of the object's methods.
This approach works well for small objects containing simple data types. The disadvantage is that whenever you need a modified object, a new object has to be created. This may result in creating a lot of small and short-lived objects that have to be garbage collected. One alternative when using an immutable object is to also create a mutable wrapper similar to the thread-safe wrapper.
An example is the String and StringBuffer class in Java. The String class is immutable while its companion class StringBuffer is not. This is part of the reason why many Java performance books recommend using StringBuffer instead of String concatenation. See "PERF-28: StringBuffer" in this section for further discussion on String and StringBuffer.
Some Java objects (such as Hashtable, Vector, and StringBuffer) already have synchronization built into many of their APIs. They may not require additional synchronization.
Some Java variables and operations are not atomic. If these variables or operations can be used by multiple threads, you must use synchronization to prevent data corruption. For example:
Replace Hashtable and Vector with Hashmap, ArrayList or LinkedList if possible.
The Hashtable and Vector classes in Java are very powerful, because they provide rich functions. Unfortunately, they can also be easily misused. Since these classes are heavily synchronized even for read operations, they can present some challenging problems in performance tuning.
One customer asked us for assistance to evaluate the performance and memory leaks in one of its applications. After some instrumentation and analysis of the application, we found they used 45 Hashtables and 215 Vectors to format a very simple HTML page. For a more complicated page, we found they used over 2,000 Hashtables and 6,500 Vectors.
The Hash, Set, and Map interfaces in JDK 1.2 provide both a non thread-safe implementation and a thread-safe wrapper, so the user can have more control over synchronization. Many of the resources listed in the "References and Resources" section explain each of these interfaces in great detail. In summary:
If you can determine the number of elements, use an Array instead of an ArrayList, because it is much faster. An Array also provides type checking, so there is no need to cast the result when looking up an object.
A List holds a sequence of objects in a particular order based on some numerical indexes. It will be automatically resized. In general, use an ArrayList if there are many random accesses. Use a LinkedList if there are many insertions and deletions in the middle of the list.
A Map is an associated array which associates any one object with another object. Use a HashMap if the objects do not need to be stored in sorted order. Use TreeMap if the objects are to be in sorted order. Since a TreeMap has to keep the objects in order, it is usually slower than a HashMap.
Replace a Vector with an ArrayList or a LinkedList.
Replace a Stack with a LinkedList.
Replace a Hashtable with a HashMap or a TreeMap.
Vector, Stack, and Hashtable are synchronized views of List and Map. You can create the equivalent of a Hashtable using:
private Map hashtable = Collections.synchronizedMap (new HashMap());
However, bear in mind that even though methods in these synchronized views are thread-safe, iterations through these views are not safe. Therefore, they must be protected by a synchronized block.
In Java's HashMap or TreeMap implementation, the hashCode()
method on the key is invoked every time the key is accessed. If the hash key is a String, each access to the key will invoke the hashCode()
and the equals()
methods in the String class. Prior to JDK release 1.2.2, the hashcode()
method in the String class does not cache the integer value of the String in an int variable; it has to scan each character in the String object each time. Such operations can be very expensive. In fact, the longer the length of the String, the slower is the hashCode()
method.
Reuse objects instead of creating new ones if possible.
Object creation is an expensive operation in Java, with impacts on both performance and memory consumption. The cost varies depending on the amount of initialization needed when the object is created. Here are ways to minimize excess object-creation and garbage-collection overhead:
Examples of resource objects are threads, JDBC connections, sockets, and complex user-defined objects. They are expensive to create, and pooling them reduces the overhead of repetitively creating and destroying them. On the down side, using a pool means you must implement the code to manage it and pay the overhead of synchronization when you get or remove objects from the pool. But the overall performance gain from using a pool to manage expensive resource objects outweighs the overhead.
Be cautious when implementing a resource pool. We have often seen the following mistakes in customers' pool management:
These mistakes can have severe consequences, including data corruption, memory leaks, a race condition, or even a security problem. Our advice in managing your pool is: keep your algorithm simple.
The "Servlets and JavaServer Pages: Design and Implementation" section includes examples showing how you can use Oracle's built-in JDBC connection caching and the servlet's SingleThreadModel to help manage a shared pool without implementing it yourself.
Recycling objects is similar to creating an object pool. But there is no need to manage it, because the pool has only one object. This approach is most useful for relatively large container objects (such as Vector or Hashtable) that you want to use for holding temporary data. Reusing these objects instead of creating new ones each time can avoid memory allocation and reduce garbage collection.
Similar to using a pool, you must take precautions to clear all the elements in any recycled object before you reuse it to avoid memory leaks. You can use the clear()
method built in to the collection interfaces. If you are building your object, you should remember to include a reset()
or clear()
method if necessary.
If initialization of an object is expensive or if an object is needed only under specific conditions, then defer creating it until it is needed.
public class myClass { private mySpecialObject myObj; ... public mySpecialObject getSpecialObject() { if (myObj == null) myObj = new mySpecialObject(); return myObj; } ... }
Avoid creating objects or performing operations that may not be used.
This mistake occurs most commonly in tracing or logging code that has a flag to turn the operation on or off during runtime. Some of this code goes to great lengths creating and formatting output without checking the flag first, creating many objects that are never used when the flag is off. This mistake can be quite expensive, because tracing and logging usually involve many String objects and operations to translate the message or even access to the database to retrieve the full text of the message. Large numbers of debug or trace statements in the code make matters worse.
Use StringBuffer instead of String concatenation.
The String class is the most commonly used class in Java. Especially in Web applications, it is used extensively to generate and format HTML content.
String is designed to be immutable; in order to modify a String, you have to create a new String object. So String concatenation can result in creating many intermediate String objects before the final String can be constructed. StringBuffer is the mutable companion class of String; it allows you to modify the String. Therefore, StringBuffer is generally more efficient than String when concatenation is needed.
Using the "+=" operations on a String repeatedly is expensive. For example:
String s = new String(); [do some work ...] s += s1; [do some more work ...] s += s2;
Replace the above String concatenation with a StringBuffer.
StringBuffer strbuf = new StringBuffer(); [do some work ...] strbuf.append(s1); [so some more work ...] strbuf.append(s2); String s = strbuf.toString();
String and StringBuffer perform the same in some cases, so you do not need to use StringBuffer directly. Optimization is done automatically by the compiler, as illustrated in Table 2-1. In these cases, there is no need to use StringBuffer directly.
Before Compiling | After Compiling |
---|---|
|
|
|
|
The default character buffer for StringBuffer is 16. When the buffer is full, a new one has to be re-allocated (usually at twice the size of the original one). The old buffer will be released after the content is copied to the new one. This constant reallocation can be avoided if the StringBuffer is created with a buffer size that is big enough to hold the String. This:
String s = (new StringBuffer(1024)).append(s1).append(s2).toString();
will be faster than
String s = s1 + s2;
The Oracle HTTP Server is an Apache-based Server. You can find much of this tuning information for the Apache server in "Chapter 4: Optimizing HTTP Server Performance" in the Oracle HTTP Server Performance Guide in the Oracle9i Application Server Platform-specific Documentation or from other Apache resources (see the "References and Resources" section).
We are simply listing some of the directives that have a performance impact and that you should examine and tune on your Web server.
Tune TCP/IP parameters.
Setting TCP/IP parameters can improve Apache server performance dramatically. We have listed the TCP/IP settings that we recommend for Solaris, along with a detailed explanation for each of these parameters, in the Oracle HTTP Server Performance Guide in the Oracle9i Application Server Platform-specific Documentation.
Tune KeepAlive directives.
The KeepAlive, KeepAliveTimeout and MaxKeepAliveRequests directives are used to control persistent connections. Persistent connections are supported in HTTP1.1 to allow a client to send multiple sequential requests through the same connection.
Setting KeepAlive to "On" allows Apache to keep the connection open for that client when the client requests it. This can improve performance, because the connection has to be set up only once. The tradeoff is that the HTTPD server process cannot be used to service other requests until either the client disconnects, the connection times out (controlled by the KeepAliveTimeout directive), or the MaxKeepAliveRequests value has been reached.
You can change these KeepAlive parameters to meet your specific application needs, but you should not set the MaxKeepAliveRequests to 0. A value of 0 in this directive means there is no limit. The connection will be closed only when the client disconnects or times out.
You may also consider setting KeepAlive to "Off" if your application has a large population of clients who make infrequent requests.
Tune MaxClients directive.
The MaxClients directive controls the maximum number of clients who can connect to the server simultaneously. This value is set to 256 by default. You can configure this parameter to a maximum of 1024 if you are using Oracle9i Application Server release 1.0.2 and later.
If your requests have a short response time, you may be able to improve performance by setting MaxClients to a lower value than 256. However, when this value is reached, no additional processes will be created, causing other requests to fail. In general, increasing the value of MaxClients does not improve performance when the system is saturated.
If you are using persistent connections, you may require more concurrent HTTPD server processes, and you may need to set the MaxClients directive to a higher value. You should tune this directive according to the KeepAlive parameters.
Avoid any DNS lookup.
Any DNS lookup can affect Apache performance. The HostNameLookups directive in Apache informs Apache whether it should log information based on the IP address (if the directive is set to "Off"), or look up the hostname associated with the IP address of each request in the DNS system on the Internet (if the directive is set to "On").
We found that performance degraded by a minimum of about 3% in our tests with HostNameLookups set to "On". Depending on the server load and the network connectivity to your DNS server, the performance cost of the DNS lookup could be much higher. Unless you really need to have host names in your logs in real time, it is best to log IP addresses and resolve IP addresses to host names offline.
Turn off access logging if you do not need to keep an access log.
It is generally useful to have access logs for your Web server, both for load tracking and for the detection of security violations. However, if you find that you do not need these data, you should turn access logging off and reduce the overhead of writing data to this log file.
Tune the SSLSessionCacheTimeout directive if you are using SSL.
The Apache server in Oracle9iAS caches a client's SSL session information by default. With session caching, only the first connection to the server incurs high latency.
In a simple test to connect and disconnect to an SSL-enabled server, the elapsed time for 5 connections was 11.4 seconds without SSL session caching as opposed to 1.9 seconds when session caching was enabled.
The default SSLSessionCacheTimeout is 300 seconds. Note that the duration of an SSL session is unrelated to the use of HTTP persistent connections. You can change the SSLSessionCacheTimeout directive in the httpd.conf
file to meet your application needs.
Use FollowSymLinks and not SymLinksIfOwnerMatch.
The FollowSymLinks and SymLinksIfOwnerMatch options are used by Apache to determine if it should follow symbolic links. If the SymLinksIfOwnerMatch option is used, Apache will check the symbolic link and make sure the ownership is the same as that of the server.
Set AllowOverride to None.
If the AllowOverride directive is not set to None, Apache will check for directives in the .htaccess
files at each directory level until the requested resource is found for each URL request. This can be extremely expensive.
Use DMS and Apache server status to monitor the performance of the Oracle HTTP Server.
See "Dynamic Monitoring Service (DMS)" for more information.
Performance testing is central to performance analysis and evaluation. Here are some general tips. Many of them are self-explanatory and are included in this chapter just as a reminder.
Understand the performance methodology.
Terms such as throughput, response time, latency, concurrency, and think time are commonly used in performance analysis. You should understand these terms, how they are used, and how they should be interpreted when running performance tests.
Determine your performance goals and set realistic performance expectations.
The performance of an application usually varies depending on the load of the system. Set realistic goals for performance at various load levels of the system.
Perform incremental performance evaluation during the development cycle.
Do not wait until the end of the project to do a test cycle. Performance tests should be run regularly after each stage of development. New performance test suites should be added as new features or functions are implemented. If possible, run performance regression tests regularly to compare performance results.
Run your performance tests on systems that simulate your production environment.
Developers commonly run their functional tests in single-user mode on their workstations or their desktops, which usually have only one CPU. This setup rarely represents the production environment, and it is not adequate for running performance tests. If the application is intended to be used by a large number of concurrent users running on multiple processors, simulate the production environment with a representative workload to study the performance impact.
Understand how to configure your test driver and analyze the result.
Commercial drivers used to simulate HTTP requests can be very effective, but they are often complicated to configure. We have encountered situations where customers set up their driver incorrectly, did not know how to interpret the results, ended up drawing the wrong conclusions, and wasted valuable time in locating the real problems.
Assign someone who is experienced in running and analyzing performance tests.
Running performance tests is not a push-button job that can be delegated to an inexperienced engineer. It requires someone who has knowledge and experience with operating systems and databases and who understands the application that is to be analyzed.
Dynamic Monitoring Service (DMS) is a feature introduced in Oracle9iAS release 1.0.2.1. It has a set of built-in metrics for the Oracle HTTP Server and the JServ process. For a detailed description of DMS, including an explanation of the metrics that it provides and how you can use DMS to evaluate and monitor the performance of your system, see Using DMS with Oracle9iAS 1.0.2.2.
The following are samples of some of these built-in metrics. In Oracle9iAS release 1.0.2.1 and later you can display the statistics on a browser or save them in a file to review at a later time. In a future release of Oracle9iAS, Oracle Enterprise Manager will provide a comprehensive graphical user interface displaying these metrics in a more interactive form.
A sample servlet using connection caching and statement caching.
import java.io.*; import java.lang.*; import java.text.*; import java.util.*; import java.sql.*; import javax.sql.*; import oracle.jdbc.driver.*; import oracle.jdbc.pool.*; import javax.servlet.*; import javax.servlet.http.*; public class JSampleConnectionCache extends HttpServlet { // Constants private final static String DBSERVER_PARAM = "DBServer"; private final static String MAXCONN_PARAM = "MaxConnections"; private final static String USERID = "userid"; private final static String GET_STMT = "begin GET_PERFSAMPLE_DATA(?,?,?,?,?,?,?,?); end;"; private final static String RC_NO_ERROR = "0"; private final static String RC_NOT_FOUND = "100"; private final static String HTML_TITLE = "<head><title> JSampleConnectionCache </title></head>"; private static OracleConnectionCacheImpl connCache = null; //-------------------------------------------------------------------------- // // Servlet life cycle methods // //-------------------------------------------------------------------------- public void init(ServletConfig config) throws ServletException { // Get configuration information. String dbServer = config.getInitParameter(DBSERVER_PARAM); if (dbServer == null) { throw new ServletException("getInitParameter Exception: " + DBSERVER_PARAM + dbServer); } int maxConn = 0; try { maxConn = Integer.parseInt(config.getInitParameter(MAXCONN_PARAM)); } catch (Exception e) { throw new ServletException("getInitParameter Exception: " + MAXCONN_PARAM + maxConn); } // Set up a connection cache. try { setupConnectionCache(dbServer, maxConn); } catch (Throwable e) { throw new ServletException(e.toString()); } } public void destroy() { // Close the connection cache created by this instance. try { if (connCache != null) connCache.close(); } catch (SQLException e) { } } //-------------------------------------------------------------------------- // // doGet() is the servlet's main entry for every request. // //-------------------------------------------------------------------------- public void doGet (HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws ServletException, IOException { long execTime = 0; Connection dbConn = null; CallableStatement dbStmt = null; try { String userName = null; String employer = null; String allowances = null; String addWithholding = null; PrintWriter httpOut; // Initialize HTTP response header. httpResponse.setContentType("text/html"); httpOut = httpResponse.getWriter(); httpOut.println("<html>"); httpOut.println(HTML_TITLE); // Get user id parameter. String userid = httpRequest.getParameter(USERID); if (userid == null) { pgError(httpOut, "Unable to retrieve user information, " + USERID + " is null"); return; } // Get data from database. try { // Get a connection from the connection cache. dbConn = connCache.getConnection(); // Create the prepare statement. dbStmt = dbConn.prepareCall(GET_STMT); if (((OracleCallableStatement)dbStmt).creationState() == OracleStatement.NEW) { dbStmt.registerOutParameter (2, Types.VARCHAR); dbStmt.registerOutParameter (3, Types.VARCHAR); dbStmt.registerOutParameter (4, Types.VARCHAR); dbStmt.registerOutParameter (5, Types.VARCHAR); dbStmt.registerOutParameter (6, Types.VARCHAR); dbStmt.registerOutParameter (7, Types.VARCHAR); dbStmt.registerOutParameter (8, Types.VARCHAR); } dbStmt.setString (1, userid); dbStmt.execute(); String retCode = dbStmt.getString(8).trim(); if (retCode.equals(RC_NO_ERROR)) { String lastName = (dbStmt.getString(2)).trim(); String firstName = (dbStmt.getString(3)).trim(); String middleName = (dbStmt.getString(4)).trim(); employer = (dbStmt.getString(5)).trim(); allowances = (dbStmt.getString(6)).trim(); addWithholding = (dbStmt.getString(7)).trim(); userName = firstName + " " + middleName + " " + lastName; pgOut(httpOut, userid, userName, employer, allowances, addWithholding); } else if (retCode.equals(RC_NOT_FOUND)) { pgError(httpOut, "Record not found for " + userid); } else { pgError(httpOut, "Unable to retrieve record for " + userid + " (SQLERROR " + retCode); } } catch (SQLException se) { throw new ServletException(se.toString()); } catch (Throwable e) { throw new ServletException(e.toString()); } } finally { try { // Release the statement object. if (dbStmt != null) dbStmt.close(); // Release the logical connection to the connection cache. if (dbConn != null) dbConn.close(); } catch(SQLException s) { throw new ServletException(s.toString()); } } } // doGet //-------------------------------------------------------------------------- // Servlet private methods //-------------------------------------------------------------------------- // // Setup database connection cache. private void setupConnectionCache(String dbServer, int maxConn) throws Throwable { try { OracleConnectionPoolDataSource connDataSource = new OracleConnectionPoolDataSource(); connDataSource.setURL("jdbc:oracle:thin:@"+dbServer); connDataSource.setUser("scott"); connDataSource.setPassword("tiger"); connCache = new OracleConnectionCacheImpl(connDataSource); // Set maximum number of pooled connections for this cache instance. connCache.setMaxLimit(maxConn); // Set cache scheme. connCache.setCacheScheme(OracleConnectionCacheImpl.FIXED_RETURN_NULL_SCHEME); // Set statement cache size. connCache.setStmtCacheSize(1); } catch (Throwable e) { throw e; } } // // Return the HTML page. private void pgOut (PrintWriter httpOut, String userid, String userName, String employer, String allowances, String addWithholding) { httpOut.println("<body>"); httpOut.println("<h1>User: <font color=blue>" + userid + "</font> </h1>"); httpOut.println("<h1>Name: <font color=blue>" + userName + "</font> </h1>"); httpOut.println("<h1>Employer: <font color=blue>" + employer + "</font> </h1>"); httpOut.println("<h1>Number of Allowances: <font color=blue>" + allowances + "</font> </h1>"); httpOut.println("<h1>Additional Amount of Withholding: <font color=blue>" + addWithholding + "</font> </h1>"); httpOut.println("</body>"); } // pgOut // // Print HTML error page. private void pgError (PrintWriter httpOut, String errmsg) { httpOut.println("<body>"); httpOut.println("<h1> <font color=red> ERROR: " + errmsg + "</font> </h1>"); httpOut.println("<hr>"); httpOut.println("</body>"); } // pgError } // JSampleConnectionCache
http://otn.oracle.com/docs/index.htm
|
Copyright © 2001 Oracle Corporation. All Rights Reserved. |
|