The Java servlet architecture provides an excellent framework for
server-side processing. Because servlets are written in Java, they can take
advantage of Java's memory management and rich set of APIs. Servlets can also
run on numerous platforms and HTTP servers without change. Further, the servlet
architecture solves the major problem of the CGI architecture: a servlet
request spawns a lightweight Java thread, while a CGI request will spawn a
complete process. Unless the project has a large budget for high-end Java-based
application server projects, servlets offer the best technology to develop
server-side components and Web-based applications.
This article, however, is not about the many
advantages of the servlet architecture (see Resources for
articles covering that); rather, it is about how to use servlets to generate
responses efficiently, use less memory in the process, and stream the generated
responses back to the client, making response times seem even better. Applying
the techniques in this article will allow you to build servlets that can serve
more requests and serve them up faster. Based on that foundation, you can build
enterprise Web applications that will scale to your needs, run 24x7, and
provide the best response times.
To aid my discussion, I use a simple example servlet that counts down from
10 to 1. Each example of the countdown servlet is slightly modified to
demonstrate a specific technique. By focusing on the techniques, rather than a
fancy example, you'll see how to apply the specific techniques to solve
problems in your own servlets.
Note: This article assumes you're familiar with the servlet architecture
and have experience writing servlets. See Resources for tutorial information.
Effective servlets
There are two major goals to building servlets for efficient HTML
generation. The first goal, especially important for high-volume Web
applications, is to carefully control the amount of garbage created during page
generation. The more garbage created, the fewer pages generated, and the slower
the response time for a request. This bottleneck occurs because the Java VM has
to steal cycles to run garbage collection when it should be generating pages.
If too much garbage is created, request processing could halt entirely. This is
unacceptable for critical Web applications. These problems can be largely
avoided using simple ecological techniques: reduce, reuse,and recycle. We'll
discuss these techniques in more detail in just a moment.
The second goal of efficient HTML generation is to keep the browser active:
the typical Web surfer doesn't have much tolerance for blank gray pages and
"waiting for response" messages. The time a Web application spends
querying a database or accessing a legacy system is typically the time a user
spends staring at a blank page. This can -- and should -- be avoided using
servlet response streaming techniques. As we'll see, these techniques dovetail
well with the aforementioned ecological techniques, and together they form a
foundation for building solid, well-behaved servlets.
Servlet response streaming techniques are also useful for much more than just
generating HTML. For servlets, they can be applied when generating XML,
JavaScript, GIF or JPEG images, or any other protocol servlets support. In
addition, the garbage management techniques we'll cover can be applied to
almost any area in Java, especially to applications that must run for an
extended period of time.
So let's get started.
Managing garbage
Garbage collection is one of the most attractive features of Java, and
servlets can take full advantage of it. Garbage collection still requires a
certain amount of overhead, which can be a problem -- especially for
server-side applications that generate Web pages dynamically. Server-side
applications typically run continuously for extended periods of time. Memory
problems that might go unnoticed in client-side applications (like applets or
Java GUI applications) could greatly effect the performance of a server-side
application in just a few hours of continuous, high-volume operation.
Web page generation requires special consideration, as it uses extensive
string manipulation and concatenation. Remember, all String
s in Java are immutable, so it's
easy to unintentionally create multiple String
objects instead of just one. Creating many unnecessary objects can be
considered "ecologically unsound" because additional effort -- in the
form of CPU cycles -- is required to clean up the garbage. The more garbage
created, the more effort required.
By applying our three simple ecological principles -- reduce the number of
objects created, reuse the objects that are created, and recycle objects that
are no longer needed -- we can develop servlets that are capable of operating
for longer periods of time with less overhead. This process results in Web
applications that are more robust and better behaved.
Let's take a look at what each of
these principles means in practice.
Reduce
One of the most effective ways to
avoid the overhead of garbage collection is simply to reduce the number of
objects being created. At first this may sound naive, but thinking carefully
about how the objects are being used can lead to simplified and more efficient
code. Take the following example:
Example 1
public
void doGet ( HttpServletRequest req, HttpServletResponse res )
throws ServletException, IOException
{
res.setContentType( "text/html" );
String html = "<html><head><title>Example
1</title></head>\n";
html += "<body>\n";
html +=
"<h1>Countdown</h1>\n";
for ( int i = 10; i > 0; i-- ) {
html += "<h1>" + i + "</h1>";
}
html += "<h1>Liftoff</h1>\n";
html += "</body></html>\n";
PrintWriter out = res.getWriter();
out.println( html );
out.close();
}
Note: This example, while slightly
contrived to demonstrate the point, represents what a servlet might look like
if ported directly from a Perl CGI script.
The above implementation will create
14 String s and 13 StringBuffer s. Most of these objects are created inside the for loop; each time through the loop a String and a StringBuffer pair are created. A much cleaner way to implement the same
method would be to simply write the output directly to the ServletResponse 's PrintWriter :
Example 2
public void doGet ( HttpServletRequest req , HttpServletResponse res )
throws ServletException , IOException
{
res. setContentType ( "text/html" );
PrintWriter out = res . getWriter ();
out . println ( "<html><head><title>Example 2</title></head>" );
out . println ( "<body>" );
out . println ( "<h1>Countdown</h1>" );
for ( int i = 10 ; i > 0 ; i -- ) {
out . print ( "<h1>" );
out . print ( i );
out . println ( "</h1>" );
}
out . println ( "<h1>Liftoff</h1>" );
out . println ( "</body></html>" );
out . close ();
}
This example generates the same HTML
output without creating any objects at all. The difference here is that all
output is written directly to the PrintWriter , which does not require creating any intermediate objects. Because
fewer objects are created and less memory is used, garbage collection is needed
much less frequently. This means more pages can be generated -- and more
requests served -- with the same resources.
You can download all the examples in
this article from Resources. Each is contained in fully functional servlets,
named Example1.java, Example2.java, etc. The actual source code contains
comments about the servlet functions, but the examples are intended for use
with this article.
Reuse
It isn't always reasonable, or even
possible, to simply write all output directly to the PrintWriter provided in the ServletResponse . For example, in situations where a portion of the HTML is
going to be generated before the response is served, it makes sense to use some
sort of buffering mechanism. As I demonstrated in our first example, the String object can't be used as a buffer. Behind the scenes, the
Java compiler really creates a StringBuffer and a String for each line of code that does any string manipulation.
This doesn't help reduce the number of objects being created.
A StringBuffer is the best way to manipulate string values. You can create
a StringBuffer , use it
to concatenate several String s
or other values (including primitives), and then convert it into a single String . Further, the String and StringBuffer will work together, sharing the same memory until the StringBuffer is modified. The following example shows how you can use a StringBuffer instead of String concatenation:
Example 3
public void doGet ( HttpServletRequest req , HttpServletResponse res )
throws ServletException , IOException
{
res. setContentType ( "text/html" );
StringBuffer buffer = new StringBuffer ( 1024 );
buffer. append ( "<html><head><title>Example 3</title></head>" );
buffer. append ( "<body>" );
buffer. append ( "<h1>Countdown</h1>" );
for ( int i = 10 ; i > 0 ; i -- ) {
buffer. append ( "<h1>" );
buffer. append ( i );
buffer. append ( "</h1>" );
}
buffer. append ( "<h1>Liftoff</h1>" );
buffer. append ( "</body></html>" );
PrintWriter out = res . getWriter ();
out . println ( buffer . toString () );
out . close ();
}
In this example, the StringBuffer isn't altered after the call to toString() , so the char array backing the StringBuffer is never copied, which reduces the memory use. The
countdown loop in this example demonstrates yet another memory reduction
technique. It could have been written
buffer.append(
"<h1>" + i + "</h1>" );
but this would have resulted in a String and StringBuffer being created for each pass through the loop.
Recycle
One final note about managing
garbage: the garbage collector can't collect garbage it thinks is still being
used. Make sure at the end of your page-generation process that all unneeded
objects are no longer referenced. If the servlet is completely stateless --
that is, it doesn't need any data to persist between requests -- all objects
should be eligible for garbage collection. Keeping references to unneeded
objects is similar to a memory leak; memory will grow with each request
serviced, but the garbage collector won't be able to reclaim the memory.
Even though Java's garbage
collection mechanism greatly reduces the hassle of memory management, there are
still some tricks to making garbage collection a little more effective. Create
fewer objects, reuse objects, and make sure the garbage collector knows when objects
are no longer used. This will help your servlets lead longer and happier lives.
Streaming output
While efficient servlets are important, efficiency isn't our sole concern.
Servlets, assuming they aren't trivial examples like the ones we've covered so
far, actually have to perform an operation; querying a database using JDBC or
accessing an external system using CORBA. The most efficient servlet ever
written can't speed up a complex DB2 query on an old mainframe. The end user
doesn't care about effective memory use; the end user wants results. And
waiting for a query to complete before responding to a request won't get the
job done. Obviously, some bottlenecks on the back end can't be avoided, but the
user doesn't have to know about it. By streaming output directly to the
browser, your end users will be blissfully unaware of the back end bottleneck.
Before jumping into the solution, let's take a closer look at the problem.
Here is another look at our countdown servlet, this time with a sleep()
added to simulate accessing an
external system:
Example 4
public void doGet ( HttpServletRequest req , HttpServletResponse res )
throws ServletException , IOException
{
res. setContentType ( "text/html" );
PrintWriter out = res . getWriter ();
out . println ( "<html><head><title>Example 4</title></head>" );
out . println ( "<body>" );
out . println ( "<h1>Countdown</h1>" );
for ( int i = 10 ; i > 0 ; i -- ) {
out . print ( "<h1>" );
out . print ( i );
out . println ( "</h1>" );
try {
// simulate access to an external system
Thread . sleep ( 1000 );
}
catch ( InterruptedException e ) {}
}
out . println ( "<h1>Liftoff</h1>" );
out . println ( "</body></html>" );
out . close ();
}
In this example, I've added a 1-second sleep()
to the countdown
loop. While this works nicely for the specific example, the real purpose of the
pause to is to demonstrate the effects of the lag time created by accessing a
database or legacy system. Here the effect isn't quite as expected: instead of
seeing the numbers counting down with a 1-second interval between each, we get
one 10-second pause and then the whole page is displayed at once. This happens
because the PrintWriter
is buffered and nothing is sent back to the browser until the output stream is close()
d at the end of the
method.
ConversionConversion EmoticonEmoticon