JSP Lifecycle and Translation

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Jakarta Pages 4.0 (Jakarta EE 11), together with Jakarta Servlet 6.1, Jakarta Expression Language 6.0 and Jakarta Standard Tag Library 3.0, as implemented by Apache Tomcat 11.0.26 (the Jasper engine) on Java 21. Everything was verified on 2026-10-08: the facts against the Pages 4.0 specification, the HttpJspPage and JspPage Javadoc and the Tomcat 11 Jasper documentation, and every example by building it into a Maven WAR (g3lifecycle.war) and running it on Tomcat 11.0.26 with curl; the outputs, generated sources and log lines shown are the ones that run produced. A JSP page is not interpreted. The container translates it into the source of a Java servlet class, compiles that class, loads it, creates one instance and calls it from many threads, and this page follows that path step by step: the two phases the specification defines, the three methods of the page contract, the generated servlet read straight from the work/ directory, the threading model that follows from a single instance, how and when the class is rebuilt, how errors are mapped back to JSP lines, how the response is buffered, and how to translate pages ahead of time. The running scenario is the Bookshelf, a small catalogue of books, here reduced to the pages that make the lifecycle visible.

A JSP Page Is a Servlet in Waiting: Two Phases

The specification describes the life of a page as two phases. In the translation phase the container validates the page (and the tag files and tag libraries it uses) and determines the JSP page implementation class that corresponds to it. In the execution phase the container manages one or more instances of that class in response to requests and other events. The specification allows translation at any moment between the deployment of the page and the processing of the first client request, and deliberately leaves the choice open as a quality-of-service decision of the container. Tomcat translates lazily, on the first request, unless the page is declared with load-on-startup (shown below), and rebuilds the class when the source changes.

Table 1. The two phases of a page’s life and what happens in each (Tomcat 11.0.26 behaviour, rows marked "observed" were reproduced)
Phase What the container does Typical failure

Translation

Parses the page, processes directives (page, include, taglib), resolves tag libraries and runs their validators, then writes the Java source of the implementation class

A syntax error in the JSP, an unknown tag, an invalid directive attribute (observed: [Page directive] has invalid attribute), a taglib URI that cannot be found. Reported as JasperException

Compilation

Compiles that source to a .class file (Tomcat bundles the Eclipse compiler ecj-4.40.jar; no JDK is needed at run time)

A Java error inside a scriptlet or declaration (observed: Unable to compile class for JSP), with the offending JSP line and the surrounding source

Class loading and initialisation

Loads the class in the web application’s class loader, creates one instance, calls init(ServletConfig), which calls jspInit() and then the generated _jspInit()

An exception in jspInit leaves the page unavailable

Execution (request time)

Calls service, which calls the generated _jspService(request, response) on the request’s thread, for every request, concurrently

Any run-time exception (NullPointerException, JspException, PropertyNotFoundException); it is reported against the JSP line through the SMAP

Destruction

Calls destroy(), which calls jspDestroy() and then _jspDestroy(), when the web application stops or when the page is recompiled and the old class is discarded (observed)

Resources opened in jspInit and not released in jspDestroy

The same path as a diagram. The upper half runs once per page (and again whenever the source changes); the lower half runs once for every request.

flowchart TD src["lifecycle.jsp
(source in the WAR)"] --> chk{"Is a compiled class
available and up to date?"} chk -->|"no: first request, or source changed"| tr["Translation
parse, validate, generate
lifecycle_jsp.java"] tr --> cp["Compilation
lifecycle_jsp.class
with an SMAP line table"] cp --> old{"An older instance
of the page exists?"} old -->|yes| dst["old instance: destroy()
runs jspDestroy"] old -->|no| ld dst --> ld["Load the class, create ONE instance
init(config) runs jspInit, then _jspInit"] chk -->|yes| svc ld --> svc["service() runs _jspService(request, response)
on the request thread, many threads at once"] svc --> buf["JspWriter buffer
8 KB by default"] buf --> resp["Response committed: headers, then body"] stop["Web application stops"] -.-> dst2["destroy() runs jspDestroy"]
Figure 1. From JSP source to response: the translation path and the request path

The whole of this machinery is the container’s job: a JSP author writes the page and, at most, jspInit and jspDestroy. The class that results is an ordinary servlet, which is why everything on the Servlet Fundamentals page (one instance, many threads, filters, error pages, dispatchers, sessions) applies to a page unchanged.

The Contract: JspPage, HttpJspPage and the Three Methods

Chapter 11 of the specification defines the contract between a JSP container and the class that implements a page. It corresponds to the Servlet interface, extended by two interfaces in jakarta.servlet.jsp: JspPage (generic) and HttpJspPage (the HTTP form every page uses). Of the three methods of the contract, only two can be written by the page author.

Table 2. How the JSP container processes a page (specification Table JSP.11-1 with the observed Tomcat behaviour)
Method Who writes it Contract and observed behaviour

void jspInit()

Optional, in a declaration (<%! ... %>)

Called once when the page is initialised; every method of the servlet, including getServletConfig, is already usable. In Tomcat HttpJspBase.init(ServletConfig) calls jspInit() first, then the generated _jspInit()

void jspDestroy()

Optional, in a declaration

Called before the page is destroyed, at a time when no request is being serviced. HttpJspBase.destroy() calls jspDestroy(), then _jspDestroy()

void _jspService(HttpServletRequest, HttpServletResponse)

Never: generated by the container

The body of the page; invoked on every request. HttpJspBase.service(...) calls it. The Javadoc says the page author must never define it

Three details of that table matter in practice:

  • init, destroy and service are not available for redefinition. The JspPage Javadoc tells a page to redefine init() and destroy() from Servlet, but Tomcat’s HttpJspBase declares init, destroy and service as final (verified with javap), so only jspInit and jspDestroy can be overridden in a declaration. This is an inconsistency in the Javadoc, recorded in the section’s errata.

  • The page is its own servlet. getServletConfig().getServletName() returns jsp for a page reached by its path (it is served by Tomcat’s default jsp servlet), and the name given in web.xml when the page is declared as a servlet with <jsp-file> (shown below). info in the page directive becomes the return value of getServletInfo().

  • Names of the form jsp_* and jspx_*, with or without a leading underscore and in any combination of upper and lower case, are reserved by the specification (section 9.1.2). A declaration must not define such a name: the generated class already uses them for its own fields and methods.

By default the generated class extends a container-specific base class (org.apache.jasper.runtime.HttpJspBase in Tomcat), which implements HttpJspPage. The extends attribute of the page directive replaces that superclass; the author then guarantees that the replacement supports the contract. Almost nobody needs it, and it is covered with the other attributes on the Directives page of this section.

The Bookshelf Used on This Page

All the examples below are files of one WAR, deployed as g3lifecycle.war (context path /g3lifecycle) on Tomcat 11.0.26, built with Maven from the same pom.xml shape as the Getting Started page of this section (Jakarta APIs with scope provided, plus the JSTL API and implementation). The page directory is the web root; WEB-INF/web.xml declares only one servlet, shown later.

Table 3. The files of the example WAR
File Request Concept

lifecycle.jsp

/lifecycle.jsp

declarations, jspInit/jspDestroy, one instance, shared counter, generated class name

WEB-INF/views/warmup.jsp

/warmup (<jsp-file>, load-on-startup)

translation at deployment, servlet name and init parameters

hello.jsp, composed.jsp, WEB-INF/fragments/footer.jspf

/hello.jsp, /composed.jsp

recompilation, static include versus jsp:include, precompilation

broken.jsp, runtimefail.jsp

/broken.jsp, /runtimefail.jsp

translation and compilation errors versus request-time errors, line mapping

buffer.jsp, buffer-default.jsp, flush.jsp, redirect.jsp

same names

buffer size, overflow, commit, late headers, redirect

A First Page and Its Lifecycle

The first page prints a list of titles and, to make the lifecycle visible, defines jspInit and jspDestroy in a declaration, counts its own hits in an instance field and reports the class, the superclass, the identity of the instance and the thread that ran the request.

The Bookshelf page that makes the lifecycle visible (lifecycle.jsp)
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" import="java.util.List" %>
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%!
    private int hits;
    private long loadedAt;

    public void jspInit() {
        loadedAt = System.currentTimeMillis();
        getServletContext().log("lifecycle.jsp: jspInit, instance " + System.identityHashCode(this));
    }

    public void jspDestroy() {
        getServletContext().log("lifecycle.jsp: jspDestroy after " + hits + " hits");
    }
%>
<%
    synchronized (this) {
        hits++;
    }
    request.setAttribute("books", List.of("Dune", "Neuromancer"));
%>
<!DOCTYPE html>
<html>
<body>
<h1>Bookshelf</h1>
<p>class=<%= getClass().getName() %></p>
<p>super=<%= getClass().getSuperclass().getName() %></p>
<p>instance=<%= System.identityHashCode(this) %> hits=<%= hits %> thread=<%= Thread.currentThread().getName() %></p>
<p>servlet name=<%= getServletConfig().getServletName() %></p>
<ul>
<c:forEach var="t" items="${books}">
  <li>${t}</li>
</c:forEach>
</ul>
</body>
</html>

Requesting it twice returns the same instance on two different request threads, with the counter advancing (blank lines at the top of the real response come from the directives and declaration lines, which are template text; the trimDirectiveWhitespaces attribute is covered on the Directives page):

curl -s http://localhost:18080/g3lifecycle/lifecycle.jsp
curl -s http://localhost:18080/g3lifecycle/lifecycle.jsp | grep instance=
<p>class=org.apache.jsp.lifecycle_jsp</p>
<p>super=org.apache.jasper.runtime.HttpJspBase</p>
<p>instance=1937448563 hits=1 thread=http-nio-18080-exec-95</p>
<p>servlet name=jsp</p>
<ul>
  <li>Dune</li>
  <li>Neuromancer</li>
</ul>
...
<p>instance=1937448563 hits=2 thread=http-nio-18080-exec-97</p>

The container’s log (ServletContext.log goes to localhost.<date>.log in Tomcat’s logs/ directory) shows the other two events. jspInit ran once, during the first request, not at deployment; jspDestroy ran when the web application was removed (three requests had been served by the instance that was current at that time):

13:02:40.057 INFO [http-nio-18080-exec-95] ...ApplicationContext.log lifecycle.jsp: jspInit, instance 1937448563
13:06:22.382 INFO [Catalina-utility-2] ...ApplicationContext.log lifecycle.jsp: jspDestroy after 3 hits

The Generated Servlet, Read from the work Directory

Jasper keeps the Java source it generates (keepgenerated defaults to true) in the web application’s scratch directory, $CATALINA_BASE/work/Catalina/<host>/<context>/. The source is the best documentation of what a page really does. For the page above the files are:

work/Catalina/localhost/g3lifecycle/org/apache/jsp/lifecycle_jsp.java
work/Catalina/localhost/g3lifecycle/org/apache/jsp/lifecycle_jsp.class
work/Catalina/localhost/g3lifecycle/org/apache/jsp/WEB_002dINF/views/warmup_jsp.java

The class lives in the package org.apache.jsp, followed by the directory of the page; its name is the file name with _jsp appended. Characters that are illegal in a Java identifier are escaped (WEB-INF becomes WEB_002dINF, where 002d is the hexadecimal code of -), and the tag handler pool fields carry names such as _005fjspx_005ftagPool_005fc_005fforEach for the same reason. The file name, the package and these identifiers are implementation details of Tomcat and may differ in Jetty or GlassFish; the specification says so itself (section 9.1.3: an implementation need not perform the transformations literally), so a page must never depend on them.

The structure of a generated class follows specification Table JSP.9-1: optional imports, the superclass, the declaration section, then _jspService with the implicit-object section and the main section. This is the real source for lifecycle.jsp, with the repeated plumbing shortened where marked:

Generated source, part 1: class header, the declarations and the lifecycle methods (lifecycle_jsp.java)
package org.apache.jsp;

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.jsp.*;
import java.util.List;

public final class lifecycle_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports,
                 org.apache.jasper.runtime.JspSourceDirectives {

    private int hits;                       // the <%! %> declaration, copied verbatim
    private long loadedAt;

    public void jspInit() { ... }           // the author's methods, also copied verbatim
    public void jspDestroy() { ... }

  private static final jakarta.servlet.jsp.JspFactory _jspxFactory =
          jakarta.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;
  static {
    _jspx_dependants = new java.util.LinkedHashMap<java.lang.String,java.lang.Long>(2);
    _jspx_dependants.put("/WEB-INF/lib/jakarta.servlet.jsp.jstl-3.0.1.jar", Long.valueOf(1664453510000L));
    ...
  }

  public void _jspInit() {                 // generated: obtain the pool of tag handlers
    _005fjspx_005ftagPool_005fc_005fforEach_0026_005fvar_005fitems =
        org.apache.jasper.runtime.TagHandlerPool.getTagHandlerPool(getServletConfig());
  }

  public void _jspDestroy() {
    _005fjspx_005ftagPool_005fc_005fforEach_0026_005fvar_005fitems.release();
  }
Generated source, part 2: the body of _jspService
  public void _jspService(final jakarta.servlet.http.HttpServletRequest request,
                          final jakarta.servlet.http.HttpServletResponse response)
      throws java.io.IOException, jakarta.servlet.ServletException {

    if (!jakarta.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) { ... return; }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
            "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
        return;
      }
    }

    final jakarta.servlet.jsp.PageContext pageContext;       // implicit objects: LOCAL variables
    jakarta.servlet.http.HttpSession session = null;
    final jakarta.servlet.ServletContext application;
    final jakarta.servlet.ServletConfig config;
    jakarta.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    ...
    try {
      response.setContentType("text/html; charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response, null, true, 8192, true);
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();

      out.write('\n');                                       // template text
      synchronized (this) { hits++; }                        // the scriptlet, verbatim
      request.setAttribute("books", List.of("Dune", "Neuromancer"));
      out.write("<p>class=");
      out.print( getClass().getName() );                     // <%= ... %>
      out.write("</p>\n");
      ...
      if (_jspx_meth_c_005fforEach_005f0(_jspx_page_context))  // <c:forEach> becomes a method
        return;
      out.write("</html>\n");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof jakarta.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) { out.flush(); } else { out.clearBuffer(); }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }

Reading it gives the rules that the other pages of this section rely on.

Table 4. How each element of the page is translated (specification chapter 9 against the generated source above)
Element in the page Generated code Consequence

Template text (HTML, whitespace)

out.write("..."), one statement per source line (mappedfile defaults to true), whitespace preserved

Blank lines after directives and scriptlets reach the browser

<%! ... %> declaration

Copied into the class body, before _jspService, in page order

Fields are instance fields shared by all requests; methods are instance methods; the implicit objects are not visible here because they are locals of _jspService

<% ... %> scriptlet

Copied verbatim into the body of _jspService, in order

Local variables are per request and therefore thread safe; one scriptlet may open a block that a later scriptlet closes

<%= ... %> expression

out.print( ... )

The expression is converted to a string; no escaping happens

${...} EL in template text

out.write(PageContextImpl.proprietaryEvaluate("${t}", String.class, pageContext, null)) in Tomcat

Evaluated at request time against the page, request, session and application scopes

<c:forEach>, any custom action

A private method _jspx_meth_c_005fforEach_005f0 that gets a handler from a tag pool, sets its attributes, calls doStartTag, the body, doAfterBody, doEndTag and doFinally

Tag handlers are pooled and reused, so a handler must not keep state between uses unless it resets it

<%@ page import %>

import lines and the set _jspx_imports_classes

Imports apply to the whole translation unit

<%@ include file %>

No code of its own: the file’s content is merged into the translation unit before translation

Changing the included file changes the including page’s class (see recompilation)

<jsp:include page="/hello.jsp"/>

JspRuntimeLibrary.include(request, response, "/hello.jsp", out, false)

A request-time call to another resource; the included page has its own class and lifecycle

Standard header of _jspService

Method guard that accepts GET, POST, HEAD and OPTIONS only (outside error dispatches)

A PUT or DELETE to a JSP gets 405 from Jasper, not from the page

Final catch

handlePageException(t) after clearing the buffer if the response is not committed

Where an errorPage or an error dispatch takes over; see the Error Handling page of this section

The translation of a static include and a dynamic include is visible side by side in composed.jsp, which has both:

A page composed from a static include and a dynamic include (composed.jsp)
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<h1>Bookshelf</h1>
<%@ include file="/WEB-INF/fragments/footer.jspf" %>
<jsp:include page="/hello.jsp"/>
</body>
</html>

Its generated source writes the footer as plain template text (out.write("<footer>Bookshelf, footer version one</footer>\n")) and calls JspRuntimeLibrary.include(...) for hello.jsp; the footer appears in _jspx_dependants with its last-modified time, which is how Jasper knows the page depends on it.

One Instance, Many Threads

The generated class is a servlet and the container creates a single instance of it (specification section 2.2 of the Servlet specification: one instance per declaration unless the application is distributable), so every request to lifecycle.jsp runs _jspService on that same object, on a different thread. The consequences follow directly from the translation table above:

  • Variables declared in a scriptlet are locals of _jspService: each thread has its own copy. They are safe.

  • Fields declared in a <%! %> declaration are fields of the shared instance. They are shared mutable state, as in any servlet, and need the same care (synchronized, atomic types, or no mutation after jspInit).

  • The implicit objects request, response, out, session and pageContext are locals as well; application and config are locals initialised from the shared servlet context and configuration.

  • The page-scope attributes belong to a PageContext created for the request and released at its end, not to the instance.

The example above increments hits inside synchronized (this). Forty parallel requests against it (twenty at a time, with xargs -P 20) leave the counter exactly where it should be, on one instance:

seq 1 40 | xargs -P 20 -I{} curl -s -o /dev/null http://localhost:18080/g3lifecycle/lifecycle.jsp
curl -s http://localhost:18080/g3lifecycle/lifecycle.jsp | grep instance=
<p>instance=1937448563 hits=43 thread=http-nio-18080-exec-99</p>

(hits=43 is the two earlier requests plus the forty parallel ones plus this last one.)

The old escape hatch for a page that was not thread safe, <%@ page isThreadSafe="false" %>, made the container serialise requests with the SingleThreadModel interface. That interface was deprecated for years and was removed in Servlet 6.0, and the attribute was deprecated in Pages 3.1 and removed in Pages 4.0. On Tomcat 11 it is a translation error:

org.apache.jasper.JasperException: /threadsafe.jsp (line: [1], column: [2]) [Page directive] has invalid attribute: [isThreadSafe]

The replacement is design, not configuration: keep state out of instance fields, or protect it.

When a Page Is Translated: First Request, Startup and Servlet Declarations

By default Tomcat translates a page on the first request that reaches it (the first hit pays for translation and compilation; the instance created then serves all the others). A page can be moved to deployment time by declaring it as a servlet with <jsp-file> and <load-on-startup>, which also lets web.xml give it a name, initialisation parameters and its own URL mapping.

A page declared as a servlet, translated when the application starts (WEB-INF/web.xml)
<servlet>
    <servlet-name>bookshelf-warmup</servlet-name>
    <jsp-file>/WEB-INF/views/warmup.jsp</jsp-file>
    <init-param>
        <param-name>greeting</param-name>
        <param-value>Hello from web.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>bookshelf-warmup</servlet-name>
    <url-pattern>/warmup</url-pattern>
</servlet-mapping>
<%@ page contentType="text/plain; charset=UTF-8" %>
name=<%= getServletConfig().getServletName() %> greeting=<%= getServletConfig().getInitParameter("greeting") %>
curl -s http://localhost:18080/g3lifecycle/warmup
name=bookshelf-warmup greeting=Hello from web.xml

The generated file for warmup.jsp carries the header Generated at: 2026-10-08 11:02:36 UTC, the moment the application was deployed, while lifecycle_jsp.java, which was only requested later, carries 11:02:40: translation happened at deployment for the first and at request time for the second. Pages under WEB-INF (as here) cannot be requested directly by a client; only the mapping /warmup reaches this one, which is the right place for views. Two other things to know about the container’s page management:

  • A page that is not used does not have to stay loaded: Tomcat’s jspIdleTimeout (seconds, default -1, never) unloads idle pages and maxLoadedJsps (default -1, unlimited) keeps only the most recently used ones; unloading destroys the instance, so jspDestroy runs, and a later request creates a new instance and calls jspInit again.

  • Which files are treated as pages (the *.jsp and *.jspx mappings of Tomcat’s default jsp servlet, and jsp-property-group patterns) belongs to the Configuration and Property Groups page of this section.

Recompilation: What Changes a Class and When Tomcat Notices

In development mode (the Jasper parameter development, default true) Tomcat checks a page for changes when it is requested, but not more often than modificationTestInterval seconds (default 4); a value of 0 checks on every access. When the source, or a file the page depends on, is newer than the compiled class, the page is translated and compiled again, the old instance is destroyed (jspDestroy) and a new instance of the new class is created (jspInit). The experiment below changes hello.jsp on disk, five seconds after the previous request:

<%@ page contentType="text/plain; charset=UTF-8" %>
Hello version one
curl -s http://localhost:18080/g3lifecycle/hello.jsp
# edit hello.jsp in the exploded web application: "version one" becomes "version two"
curl -s http://localhost:18080/g3lifecycle/hello.jsp
Hello version one
Hello version two

The generated hello_jsp.java was rewritten (out.write("Hello version two\n")) and its Generated at header moved. Editing lifecycle.jsp the same way produced the log pair that shows what recompilation does to a page’s state: the old instance reported jspDestroy after 43 hits, the new instance reported jspInit, instance 157172844, and the counter on the page started again at hits=1. A page that keeps data in an instance field therefore loses it whenever the page is edited, a second reason (after concurrency) to keep state out of declarations.

Dependencies are tracked too. composed.jsp includes footer.jspf with the static include directive, and the footer is listed in _jspx_dependants. Editing only the footer (after the interval) made the next request to composed.jsp show footer version two: the including page was translated again, because a static include is part of its translation unit. Editing only hello.jsp, which composed.jsp includes with jsp:include, did not recompile composed.jsp at all; hello.jsp was rebuilt on its own the next time it was served.

Table 5. Recompilation behaviour by kind of change (the first three rows observed on Tomcat 11.0.26, the others from the Jasper HowTo and the generated source)
What changes What is rebuilt

The page itself

The page’s class (and a new instance)

A file brought in with <%@ include file="..." %>

The including page, never the fragment on its own

A page reached with <jsp:include> or <jsp:forward>

Only that page, when it is next requested

A tag library descriptor or a tag file the page uses

Jasper lists such dependencies in _jspx_dependants (shown above for c.tld and the JSTL jar) and consults that list when it checks the page

A Java class under WEB-INF/classes or lib

Not tracked by the page check; classes are reloaded only if the container reloads the whole web application

Production changes the defaults. The Jasper HowTo recommends development=false (no checks on access, so a page is never retranslated unless the application is redeployed), genStringAsCharArray=true, a high modificationTestInterval if development mode cannot be switched off, and trimSpaces=single or extended; the Jasper checkInterval (seconds, default 0) enables background recompilation, when development is false, so that the previous class keeps serving requests until the new one has compiled. The full parameter list, JspC and the Maven and Ant integrations are the topic of the Jasper Configuration & Precompilation page of this section.

Table 6. The Jasper parameters that govern the lifecycle (Tomcat 11 Jasper HowTo, $CATALINA_BASE/conf/web.xml, servlet jsp)
Parameter Default Effect on translation, compilation and recompilation

development

true

If true, pages are checked for modification on access, at most every modificationTestInterval seconds. Set false in production

modificationTestInterval

4

Seconds between modification checks in development mode; 0 checks on every access

checkInterval

0

If development is false and this is greater than zero, background compiles run at this interval

recompileOnFail

false

After a failed compilation, retry on the next access without waiting for the interval (development mode only)

keepgenerated

true

Keep the .java file of each page

suppressSmap

false

Do not write the SMAP line table into the class

dumpSmap

false

Also write the SMAP of each page to a file

mappedfile

true

Generate one out.write per template line, to ease debugging

maxLoadedJsps, jspIdleTimeout

-1, -1

Unload least-recently-used or idle pages

compilerTargetVM, compilerSourceVM

17 in 11.0

Java level of the generated source and class (the HowTo lists the current value)

Translation Errors Versus Request-Time Errors, and Line Mapping with SMAP

Because translation and compilation happen on the first request, a mistake in a page is not reported at deployment but to the first client. The two kinds of failure look different, and both are mapped back to lines of the page.

A translation or compilation error is a JasperException with the heading Unable to compile class for JSP:

A page with a Java error in a scriptlet (broken.jsp)
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<% int total = ; %>
</body>
</html>
HTTP Status 500 – Internal Server Error
org.apache.jasper.JasperException: Unable to compile class for JSP:
An error occurred at line: [4] in the jsp file: [/broken.jsp]
Syntax error on token ";", delete this token
1: <%@ page contentType="text/html; charset=UTF-8" %>
2: <html>
3: <body>
4: <% int total = ; %>
5: </body>
6: </html>

The message shows the JSP line and a few lines of context (displaySourceFragment, default true), not the generated Java. A request-time error is an exception thrown while _jspService runs. Here the page dereferences a request parameter that is absent:

A page that fails at request time (runtimefail.jsp)
<%@ page contentType="text/html; charset=UTF-8" %>
<html>
<body>
<h1>Bookshelf</h1>
<%
    String title = request.getParameter("title");
%>
<p>Title length: <%= title.length() %></p>
</body>
</html>
org.apache.jasper.JasperException: An exception occurred processing [/runtimefail.jsp] at line [8]
5: <%
6:     String title = request.getParameter("title");
7: %>
8: <p>Title length: <%= title.length() %></p>
9: </body>
10: </html>
Root Cause: java.lang.NullPointerException: Cannot invoke "String.length()" because "title" is null
        org.apache.jsp.runtimefail_jsp._jspService(runtimefail_jsp.java:130)

Jasper says line 8 of the JSP, but the stack frame in the cause says line 130 of the generated Java. The two numbers are related by the SMAP, the line table that the Jakarta Debugging Support for Other Languages specification defines and that the Pages specification requires of every container (section 11.5): the class file gets a SourceDebugExtension attribute, with a stratum named JSP, mapping lines of the page to lines of the generated class. Tomcat uses it to turn the Java line into the page line in the message, and IDE debuggers use it to step through the page rather than the generated servlet. javap -v shows the table of runtimefail_jsp.class:

SourceDebugExtension:
  SMAP
  runtimefail_jsp.java
  JSP
  *S JSP
  *F
  + 0 runtimefail.jsp
  runtimefail.jsp
  *L
  1,7:121
  7:128
  8:129,3
  9,3:132
  *E

Each *L line reads "input lines a,b map to output starting at line n`": JSP line 8 is output lines 129 to 131, and the failing statement is at 130. The same table is why a debugger breakpoint on line 8 of a JSP works, and why a stack trace of the page in the server log needs the cause section, not the first frames. With `suppressSmap=true Tomcat leaves the table out, and the message falls back to Java line numbers. In development, keep mappedfile on and read the .java in work/ when a page misbehaves in a way the message cannot explain.

Output Buffering: JspWriter, buffer, autoFlush and Commit

The implicit object out is a jakarta.servlet.jsp.JspWriter, a buffered writer that sits between the generated code and the servlet response. Headers are not sent until the response is committed, which happens when the buffer is flushed or, with autoFlush="true", fills up. Until then the page can still change the status, set headers, redirect or call sendError; after that those calls no longer work, exactly as for any servlet. Two attributes of the page directive control it, and a third affects whether the page touches the session at all.

Table 7. Page attributes that decide how a page’s output is buffered (specification section 1.10.1)
Attribute Default Meaning

buffer

buffered, at least 8kb (Tomcat generates 8192)

none (unbuffered, output goes straight to the response writer) or a size such as 16kb; the kb suffix is mandatory, or translation fails. out.getBufferSize() returns it

autoFlush

true

true flushes the buffer when it fills; false raises an exception on overflow. autoFlush="false" with buffer="none" is a translation error

session

true

If true the page gets the session object, and Tomcat creates the session (and sends a JSESSIONID cookie) the first time a page that does not say otherwise is requested; session="false" makes the session variable unavailable

Four short pages show the behaviour on Tomcat 11.0.26.

The defaults, and a header set after output but before commit (buffer-default.jsp)
<%@ page contentType="text/plain; charset=UTF-8" %>
size=<%= out.getBufferSize() %> autoFlush=<%= out.isAutoFlush() %> remaining=<%= out.getRemaining() %>
<% response.setHeader("X-Late", "set-after-output"); %>
committed=<%= response.isCommitted() %>
curl -s -D- http://localhost:18080/g3lifecycle/buffer-default.jsp
HTTP/1.1 200
Set-Cookie: JSESSIONID=3577B8A9BABA8ECB56C3AF074E57B8DA; Path=/g3lifecycle; HttpOnly
X-Late: set-after-output
Content-Type: text/plain;charset=UTF-8
Content-Length: 58

size=8192 autoFlush=true remaining=8156

committed=false

The header was added after the first line of body had been written, yet it reaches the client, because the line was still in the buffer, and the response has a Content-Length (58), which a buffered page can compute. The Set-Cookie line is the default session="true" at work, on a page that never mentions a session.

A small buffer that overflows (buffer.jsp)
<%@ page contentType="text/plain; charset=UTF-8" buffer="1kb" autoFlush="false" %>
size=<%= out.getBufferSize() %> autoFlush=<%= out.isAutoFlush() %>
<% if (request.getParameter("big") != null) {
       for (int i = 0; i < 200; i++) { out.print("0123456789"); }
   } %>
done
$ curl -s http://localhost:18080/g3lifecycle/buffer.jsp
size=1024 autoFlush=false
done

$ curl -s "http://localhost:18080/g3lifecycle/buffer.jsp?big=1"        (HTTP 500)
java.io.IOException: An exception occurred processing [/buffer.jsp] at line [4]
Root Cause: java.io.IOException: Error: JSP Buffer overflow
        org.apache.jasper.runtime.JspWriterImpl.bufferOverflow(JspWriterImpl.java:149)

Two thousand characters do not fit in 1024 bytes and, with autoFlush="false", the page fails instead of flushing. With the default autoFlush="true" the same page would flush at 1 KB, commit the response and send it chunked.

Flushing commits the response; later header changes are silently lost (flush.jsp)
<%@ page contentType="text/plain; charset=UTF-8" %>
before flush
<% out.flush(); %>
<% response.setHeader("X-Too-Late", "never-sent"); %>
committed=<%= response.isCommitted() %>
HTTP/1.1 200
Set-Cookie: JSESSIONID=BE31A4793F860C1DAE50E940F3B80D8C; Path=/g3lifecycle; HttpOnly
Content-Type: text/plain;charset=UTF-8
Transfer-Encoding: chunked

before flush


committed=true

X-Too-Late is absent: after out.flush() the headers had gone, so setHeader did nothing and the response is chunked because its length was not known when it was committed. Finally, a redirect issued from a scriptlet after some output, while the response is still uncommitted, works and the buffered text is discarded:

A buffered page that redirects (redirect.jsp)
<%@ page contentType="text/plain; charset=UTF-8" %>
this text is in the buffer and is discarded
<% response.sendRedirect(request.getContextPath() + "/hello.jsp"); %>
HTTP/1.1 302
Location: /g3lifecycle/hello.jsp
Content-Length: 0

The specification’s wording of this case (section 11.3) names the method response.redirect; the method is sendRedirect, as above. The thing that matters in design is that anything which must be decided before the first byte (status codes, redirects, cookies, content type, a forward) has to happen before the buffer is flushed, either explicitly or by overflowing; a larger buffer, or doing the work in a servlet before forwarding to the view, are the usual remedies. The Servlet response buffer, which is the layer below JspWriter, is discussed in the Servlet Fundamentals page.

Precompilation: The jsp_precompile Request and Build-Time Translation

The specification gives containers a precompilation protocol (section 11.4). Request parameter names beginning with jsp are reserved; a request to a page with the parameter jsp_precompile is a precompilation request that must not be delivered to the page itself. The parameter may have no value, true or false; true or no value is a suggestion to translate and compile the page, which the container may ignore. Any other value is an error (HTTP 500). Tomcat honours the suggestion and returns an empty 200:

curl -s -w '%{http_code} len=%{size_download}\n' "http://localhost:18080/g3lifecycle/hello.jsp?jsp_precompile=true"
curl -s -o /dev/null -w '%{http_code}\n' "http://localhost:18080/g3lifecycle/hello.jsp?jsp_precompile=foo"
200 len=0
500

After the first call, hello_jsp.java and hello_jsp.class exist in work/ although the page body never ran; the second call produced jakarta.servlet.ServletException: Cannot have precompilation request parameter [jsp_precompile] set to [foo], as the specification requires. This is a way to warm up a deployed application page by page, with a script that requests each page with the parameter. A better one for production is to translate and compile every page during the build (JspC, the Ant <jasper> task, the Jetty JspC Maven plugin) and package the result, which removes the first-request delay and the need for a compiler on the server; the specification describes this in section 1.1.9 and in Appendix A, and the tooling is the subject of the Jasper configuration and precompilation page of this section. Precompilation does not work with every feature (the HowTo points out that jsp-property-group can make it impractical), which is why the production settings above still matter.

Which Engine Does This: Jasper, WaSP and Portability

The lifecycle above is the specification’s; the generated source is Tomcat’s. Tomcat and Jetty implement Pages with Apache Jasper (Jetty names its JSP module apache-jsp), and GlassFish with Eclipse WaSP (eclipse-ee4j/wasp), which uses the Expressly EL implementation. The three produce differently shaped classes, so a page, a tag or a framework must not depend on the class name, the package, the superclass or the helper methods seen above; the supported seams are the HttpJspPage contract, the tag APIs, PageContext and the SMAP. File locations differ too: the work/ directory path shown here is Tomcat’s, so check the container’s own documentation for where it keeps generated sources.

Pitfalls and Error Names

Table 8. Lifecycle and translation pitfalls, their symptoms and fixes (rows marked "observed" were reproduced on Tomcat 11.0.26)
Symptom or error Cause Fix

JasperException: Unable to compile class for JSP on the first request, never at deployment (observed)

Translation and compilation are lazy; the error belongs to the first client

Precompile (jsp_precompile, JspC) or exercise every page in the build; read the JSP line shown, not the generated one

[Page directive] has invalid attribute: [isThreadSafe] (observed)

The attribute was removed in Pages 4.0 along with SingleThreadModel

Delete it; make state thread safe

Counters or caches in a <%! %> field disagree between users, or restart at 1 after an edit (observed)

One instance serves every request, and a recompiled page gets a new instance

Use locals, synchronized/atomics, or store shared data in the application scope or a bean; never keep per-user state in a field

Stack trace points at xxx_jsp.java:130 (observed)

The first frames are generated lines; the SMAP maps them to the page

Read Jasper’s "at line [8]" and the code fragment; keep SMAP on; open the .java in work/ only if needed

A header, cookie, status or redirect is ignored, or IllegalStateException follows (observed)

out.flush(), a full buffer, flush="true" on an include, or any output that fills the buffer committed the response

Decide before writing; raise buffer; do the work in a servlet that forwards to the view

java.io.IOException: Error: JSP Buffer overflow (observed)

autoFlush="false" and more output than the buffer holds

Remove autoFlush="false" or enlarge buffer

A JSESSIONID cookie on every page, including static-looking ones (observed)

session defaults to true

<%@ page session="false" %> on pages that never read the session

jsp_precompile=foo returns 500 (observed)

The specification makes any value other than none/true/false an error

Use true or no value

Edits to an included fragment have no effect, or they only apply after a while

A static include is part of the includer; the check interval (4 s) applies to the includer

Wait one interval, or set modificationTestInterval lower in development; changing the fragment recompiles the includer

Edits to a deployed page never show up in production

development=false: pages are never re-checked

Redeploy the application, or set checkInterval for background checks

First request after a deployment is slow

Translation, compilation and class loading of the page and its tag libraries

Precompile at build time, or declare warm-up pages with <load-on-startup>

jspInit threw an exception and the page does not serve requests

A servlet whose init fails is not put in service

Fix the cause and make jspInit robust; log the failure

Resources opened in jspInit leak after redeploy

jspDestroy was not written

Release them in jspDestroy

Generated source edited by hand has no effect

Jasper overwrites it on the next recompile

Change the JSP

A page uses Tomcat’s generated names (_jspx_meth_..., the class name)

They are implementation details of Jasper

Use only HttpJspPage, PageContext and the tag APIs

405 for PUT or DELETE on a JSP

The generated _jspService accepts GET, POST, HEAD and OPTIONS only

Handle those methods in a servlet

ClassNotFoundException: javax.servlet.jsp... on Tomcat 10 and 11

A page, tag handler or library still uses the javax names

Migrate to jakarta.* (Tomcat’s migration tool covers WARs)

What Changed Recently

Table 9. Changes in the page lifecycle contract, newest first (specification Appendix C)
Release Change relevant to this page

Pages 4.0 (Jakarta EE 11)

Minimum Java 17; isThreadSafe removed; jsp:plugin and its related actions removed; the deprecated jakarta.servlet.jsp.el classes, BodyTag.EVAL_BODY_TAG and JspException.getRootCause removed; ErrorData gains jakarta.servlet.error.query_string and jakarta.servlet.error.method; EL 6.0 removes ELResolver.getFeatureDescriptors()

Pages 3.1 (Jakarta EE 10)

isThreadSafe deprecated (because SingleThreadModel left Servlet 6.0); jsp:plugin deprecated; an option to raise PropertyNotFoundException for unknown EL identifiers (errorOnELNotFound); default imports clarified for EL

Pages 3.0 (Jakarta EE 9)

The namespace moves from javax.servlet.jsp to jakarta.servlet.jsp; deprecated methods are annotated with @Deprecated

Pages 4.1 (Jakarta EE 12, in progress)

Milestone 4.1.0-M2 is on Maven Central; nothing on this page depends on it

Tomcat 11.0.x

Implements Pages 4.0, Servlet 6.1 and EL 6.0; the default compilerTargetVM is 17; the Jasper parameters in the table above are those of the Tomcat 11.0 HowTo

Where the Books Are Outdated

Web Development with JavaServer Pages (Manning, 2001, listed in the section bibliography) describes JSP 1.2 on Tomcat 4, and its chapter on how JSP works and its appendices (the JSP 1.1 to 1.2 changes, running Tomcat, the syntax and API references) are the origin of most of what developers still remember about the lifecycle. The model is intact: translation phase and request phase, a servlet underneath, jspInit, _jspService and jspDestroy, output buffering and recompilation on change. What differs is the platform around it.

Table 10. JSP 1.2-era material and what applies today (Pages 4.0, Tomcat 11)
The book’s era Today

javax.servlet.jsp.*, javax.servlet.*

jakarta.servlet.jsp.* and jakarta.servlet.* since Pages 3.0 / Jakarta EE 9; Tomcat 10 and 11 do not run the old names

<%@ page isThreadSafe="false" %> to serialise requests with SingleThreadModel

Removed (Pages 4.0, Servlet 6.0); the page is a translation error on Tomcat 11

A JDK with its compiler on the server for translated pages

Tomcat 11 bundles the Eclipse compiler (ecj-4.40.jar) in its lib directory, so Jasper does not need the JDK’s own compiler; generated code targets Java 17 by default (compilerTargetVM)

Tomcat 4 era scratch directories and generated class names

$CATALINA_BASE/work/Catalina/<host>/<context>/org/apache/jsp/ in Tomcat 11; the layout is still container specific

Scriptlets and declarations as the normal way to write pages

Legacy: the specification keeps them, but EL, JSTL and tag files replace them; the Scripting Elements page of this section covers them and their migration

jspInit and jspDestroy as the page-level hooks

Unchanged; application-level hooks are listeners and filters, and Jasper adds its own generated _jspInit/_jspDestroy for tag pools (an implementation detail)

jsp:plugin and applets for rich clients

Removed from Pages 4.0; no major browser runs applets

Precompilation through the container’s own tools, or the jsp_precompile request parameter

The jsp_precompile protocol is unchanged and still required; build-time JspC through Ant, Maven and the container’s plugins is the production approach

JSP errors read from the generated Java stack trace

SMAP (JSR 45, now Jakarta Debugging Support for Other Languages) maps errors to page lines in messages and debuggers

Tag library validation (TagLibraryValidator on the XML view) as a translation step

Concept unchanged and part of the translation phase today (specification chapter 10); see the JSP Documents page of this section

References