Skip Headers

Oracle9i Application Server Best Practices
Release 1 (v1.0.2.2)

Part Number A95201-01
Go To Documentation Library
Library
Go To Product List
Solution Area
Go To Table Of Contents
Contents
Go To Index
Index

Go to previous page Go to next page

2
Performance and Scalability

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:

Performance and Scalability Overview

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.

Performance Tradeoffs

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: A Case Study

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:

  1. Each JSP accessible by the user (such as the home page) calls a beginRequest() method to check if the user has been authenticated based on the cookie information from the user.

  2. If the user is valid, then an entry will be added to a Hashtable with the user's profile object, using the thread identifier (ID) as the key. It will also obtain other resources that the user may need and store them in this profile object. One of these resources can be a JDBC connection.

  3. The JSP will do whatever data query or access it needs by calling the appropriate method to generate the content. It may include many nested JSPs.

  4. Any method that needs access to the user profile information will need to go to the Hashtable to get the user profile and any resource objects using the thread ID.

  5. Before returning the content to the user, the JSP will call an 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

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:

Use of Hashtables

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:

endRequest() Called Manually

The endRequest() must be called manually to release the resources before exiting the JSPs. Problems:

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.

Servlets and JavaServer Pages: Design and Implementation

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.


Note:

Because JServ is based on Servlet 2.0, we will not include any best practices in this section that require any Servlet 2.2 support. 


PERF-1: Storing State Information

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:

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.

Figure 2-1 Case Study Performance Results


Text description of perf_01.gif follows.
Text description of the illustration perf_01.gif

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:

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.

PERF-2: Connection Pooling and Caching

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:

  • "Code Example" for an example of a servlet using connection pooling and caching

  • For a detailed description and additional examples of connection pooling and connection caching, see the Oracle8i JDBC Developer's Guide and Reference in the Oracle Application Server Documentation Library

 

PERF-3: Statement Caching

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:

  • "Code Example" for an example of a servlet using connection pooling and caching

  • For a detailed description and additional examples of connection pooling and connection caching, see the Oracle8i JDBC Developer's Guide and Reference in the Oracle Application Server Documentation Library

 

PERF-4: Mid-Tier Caching

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.

See Also:

  • Oracle9iAS Web Cache Administration and Deployment Guide in the Oracle9i Application Server Documentation Library

  • Oracle9iAS Database Cache Concepts and Administration Guide in the Oracle9i Application Server Documentation Library

 

PERF-5: Database Connections

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.

PERF-6: Database and SQL Tuning

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.

See Also:

Oracle HTTP Server Performance Guide in the Oracle9i Application Server Platform-specific Documentation 

PERF-7: Memory Leaks

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:

PERF-8: Spawning Threads

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.

PERF-9: SingleThreadModel in Servlets

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:

Consider using SingleThreadModel if:

If you are using SingleThreadModel:

    1. Go to the jserv.properties file

    2. Determine the value of security.maxConnections

    3. Go to the zone.properties file

    4. Set singleThreadModelServlet.maximumCapacity to be greater than or equal to the value determined in step 2

PERF-10: Servlets Startup Operations

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:

PERF-11: Separate JSPs

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.

PERF-12: Static or Dynamic Include in JSPs

Choose static or dynamic include appropriately.

The JSP has two different include mechanisms:

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.

PERF-13: Thread-Safe JSPs

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:

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

PERF-14: Sessions in JSPs

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" %>

PERF-15: Use Forward in JSPs

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.

PERF-16: JSP Buffer

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" %>

Servlets and JavaServer Pages: Deployment

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.


Note:

Because JServ is based on Servlet 2.0, we will not include any best practices in this section that require Servlet 2.2 support. 


PERF-17: Multiple JServs

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.

PERF-18: Java Heap Size

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.

PERF-19: JServ autoreload.classes

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.

PERF-20: Preload Servlets

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.

PERF-21: Pre-translate JSPs

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 http://otn.oracle.com/docs/index.htm 

PERF-22: JSP debug flag and developer_mode

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

PERF-23: Dynamic Monitoring Service

Use DMS to monitor the performance of the JServ processes.

See "Dynamic Monitoring Service (DMS)" for more information.

Java Performance

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.

PERF-24: Synchronization

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.

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:

PERF-25: Hashtable and Vector

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.

PERF-26: Reuse Objects

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;
  }
  ...
}
    

PERF-27: Unused Objects and Operations

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.

PERF-28: StringBuffer

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.

Table 2-1  Java2 Compiler Conversion of String and StringBuffer
Before Compiling  After Compiling 

String s = "a" + "b" + "c"; 

String s = "abc"; 

String s = s1 + s2; 

String s = (new StringBuffer()).append(s1).append(s2).toString(); 

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;

Oracle HTTP Server Tuning

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.

PERF-29: TCP/IP Parameters

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.

PERF-30: KeepAlive

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.

PERF-31: MaxClients

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.

PERF-32: DNS Lookup

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.

PERF-33: Access Logging

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.

PERF-34: SSLSessionCacheTimeout

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.

PERF-35: FollowSymLinks

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.

PERF-36: AllowOverride

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.

PERF-37: DMS and Apache Server Status

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

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.

PERF-38: Performance Methodology

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.

See Also:

"Performance Overview" in the Oracle HTTP Server Performance Guide in the Oracle9i Application Server Platform-specific Documentation 

PERF-39: Performance Goals

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.

See Also:

"Performance Overview" in the Oracle HTTP Server Performance Guide in the Oracle9i Application Server Platform-specific Documentation 

PERF-40: Performance Evaluation

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.

PERF-41: Performance Testing

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.

PERF-42: Test Driver

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.

PERF-43: Testing Personnel

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)

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.

Figure 2-2 Global Metrics for the HTTP Server


Text description of perf_02.gif follows.
Text description of the illustration perf_02.gif

Figure 2-3 Metrics for Each Module Loaded by HTTP Server


Text description of perf_03.gif follows.
Text description of the illustration perf_03.gif

Figure 2-4 Global Metrics for a JServ Process


Text description of perf_04.gif follows.
Text description of the illustration perf_04.gif

Figure 2-5 JVM Metrics


Text description of perf_06.gif follows.
Text description of the illustration perf_06.gif

Figure 2-6 Metrics for an Individual Servlet


Text description of perf_05.gif follows.
Text description of the illustration perf_05.gif

Code Example

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

References and Resources

Oracle Documentation


Go to previous page Go to next page
Oracle
Copyright © 2001 Oracle Corporation.

All Rights Reserved.
Go To Documentation Library
Library
Go To Product List
Solution Area
Go To Table Of Contents
Contents
Go To Index
Index