Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 4

Understanding the Servlet Lifecycle

Recommended resources

Hello! Welcome to your fourth lesson on Java backend development.

In our last session, we got our hands dirty by implementing your first servlet. You learned how to handle GET and POST requests by overriding the doGet() and doPost() methods and how to map a URL to your servlet using the web.xml deployment descriptor.

Now that you've created a servlet, it's time to understand its life. A servlet isn't a program you run manually; it's a component managed by the Servlet Container. This lesson addresses the learning outcome: Describe the Servlet lifecycle (init, service, destroy). We'll explore the distinct phases a servlet goes through, from its birth to its death, and the key methods the container uses to manage it. This is another fundamental concept that frameworks like Spring build upon, aligning perfectly with your goal of understanding what happens "under the hood."

The Servlet Lifecycle: An Overview

Unlike a typical Java application with a main() method, a servlet's existence is controlled entirely by the Servlet Container. The container is responsible for creating the servlet instance, calling its methods to handle requests, and eventually removing it. This sequence of events is known as the servlet lifecycle.

The entire lifecycle revolves around three core methods defined in the javax.servlet.Servlet interface:

  1. init(): Called once when the servlet is first created.
  2. service(): Called for every single request.
  3. destroy(): Called once just before the servlet is taken out of service.

The following diagram provides a great visual summary of these states and transitions.

Java Servlet LifeCycle Diagram
This diagram shows the main phases of a servlet's life, from instantiation and initialization (`init`), through request handling (`service`), to its final destruction (`destroy`).

A Detailed Walkthrough of the Lifecycle

To get a deep, step-by-step understanding of the entire process, from a server starting up to a servlet being destroyed, we'll use a detailed video walkthrough. It visualizes the roles of the container, the server, and your application code.

Servlets Flow of Execution (Servlet Life Cycle)

The video 'Servlets Flow of Execution' from Durga Software Solutions provides a comprehensive, low-level explanation of the entire servlet execution flow. It will help you visualize each stage of the lifecycle.

This video is quite detailed, so we'll break it down. Watch the segments described below and focus on the key actions happening in each phase.

Let's dissect the lifecycle phase by phase, using the video as our guide.

Phase 1: Loading and Instantiation

This is the "birth" of the servlet.

  • When does it happen? By default, this happens lazily. The container only loads and creates an instance of your servlet when the first request for it arrives. Alternatively, you can configure the servlet to be loaded eagerly when the server starts up (we'll touch on this later).
  • What happens?
    1. The container finds your servlet's .class file.
    2. It loads the class into memory.
    3. It creates one single instance of the servlet by calling its no-argument constructor.

This "single instance" model is a crucial concept. The container creates only one object of your servlet class for the entire application. This single object will be used to handle all subsequent requests.

Watch: Please view the segment from 03:34 to 11:25 in the 'Servlets Flow of Execution' video. It covers server startup, how the container finds the correct servlet using web.xml, and the lead-up to instantiation.

Phase 2: Initialization (init() method)

Immediately after the servlet instance is created, the container calls the init() method.

  • When does it happen? Right after instantiation, and only once during the servlet's entire lifetime.
  • What is its purpose? The init() method is for one-time setup activities. This is the perfect place for code that is expensive to run and should not be executed on every request.
    • Establishing a database connection pool.
    • Loading configuration files or properties.
    • Initializing complex objects or services.
  • Method Signature: public void init(ServletConfig config)
    The container passes a ServletConfig object, which gives you access to initialization parameters specific to this servlet (defined in web.xml) and the ServletContext (which is shared across the whole application). When extending HttpServlet, it's good practice to call super.init(config) to ensure the parent class is properly initialized.

Watch: Now, view the part of the video from 11:25 to 14:44. It shows the container loading the class, creating the object (instantiation), and then calling the init() method (initialization).

Phase 3: Request Handling (service() method)

This is the main "work" phase of the servlet.

  • When does it happen? For every single client request that maps to the servlet. This method is called repeatedly throughout the servlet's life.
  • What is its purpose? To process the client's request and generate a response.
  • The HttpServlet Dispatcher: As we covered in the last lesson, when you extend HttpServlet, you don't usually override service(). The HttpServlet's implementation of service() inspects the HTTP method (GET, POST, etc.) and dispatches the request to the appropriate doGet(), doPost(), etc., method.

Watch: Continue with the video from 14:44 to 17:58. This part explains how the container creates request and response objects for each request and calls the service() method, which in turn leads to your doPost() logic being executed.

Thread Safety Implication

Since there is only one servlet instance, but many requests can arrive concurrently, the container handles each request in a separate thread. All these threads operate on the same single servlet object.

This means:

  • Instance variables (fields) are shared across all threads. If multiple threads modify an instance variable at the same time, you can get data corruption or race conditions. Your experience with multithreading in Java is directly applicable here.
  • Local variables (declared inside doGet/doPost) are thread-safe. Each thread gets its own copy on its stack, so there's no sharing.

Best Practice: Avoid storing request-specific or client-specific data in instance variables. If you must have mutable shared state, you must use synchronization mechanisms to protect it.

Test your understanding!

Imagine a servlet with an instance variable private int counter = 0;. Inside doGet(), you have the line counter++;. If two requests arrive at the exact same time, what is a potential problem with this simple increment operation?

Show answer

The counter++ operation is not atomic. It's actually a three-step process: read the value of counter, increment it, and write the new value back. If two threads execute this concurrently, they might both read the same initial value (e.g., 5), both increment it to 6, and both write 6 back. The counter will end up as 6, when it should be 7. This is a classic race condition. To fix this, you would need to use synchronization, for example by using java.util.concurrent.atomic.AtomicInteger.

Phase 4: Destruction (destroy() method)

This is the "end of life" for the servlet.

  • When does it happen? When the container is shutting down or when the web application is being undeployed. Like init(), the destroy() method is called only once.
  • What is its purpose? To clean up and release any resources that were acquired in the init() method. It's the logical counterpart to init().
    • Closing database connections.
    • Writing any in-memory cache data to a file.
    • Stopping background threads.
  • Method Signature: public void destroy()
    By the time destroy() is called, the container has stopped sending new requests to the servlet and will wait for any currently running requests to finish.

Watch: Finally, view the segment from 20:17 to 21:34. It clearly explains when and why the destroy() method is called to perform cleanup.


A Practical Demonstration

Let's see this in action with a concrete code example. The following video implements a simple "Request Counter" servlet that makes the lifecycle tangible.

Introduction to Java Servlets

The video 'Introduction to Java Servlets' by senturain provides a clear, practical demonstration of the lifecycle methods using a request counter example.

Watch the segment from 30:29 to 36:03. Pay close attention to how the counter instance variable is initialized in init() and then incremented in doGet(), proving that the state persists across multiple requests to the same servlet instance.

Here is what the code for such a servlet would look like, based on the video and our discussion. Note the use of System.out.println to trace the lifecycle in the server logs.

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.atomic.AtomicInteger;

public class LifecycleCounterServlet extends HttpServlet {

    private AtomicInteger counter; // Using AtomicInteger for thread safety

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        counter = new AtomicInteger(0); // Initialize ONCE
        System.out.println("LifecycleCounterServlet: init() called. Servlet is ready.");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // Increment the counter for EACH request
        int currentCount = counter.incrementAndGet();
        System.out.println("LifecycleCounterServlet: doGet() called. Current count: " + currentCount);

        resp.setContentType("text/html");
        PrintWriter out = resp.getWriter();
        out.println("<html><body>");
        out.println("<h1>Servlet Lifecycle Demo</h1>");
        out.println("<p>This servlet has been accessed " + currentCount + " times.</p>");
        out.println("</body></html>");
    }

    @Override
    public void destroy() {
        System.out.println("LifecycleCounterServlet: destroy() called. Servlet is being taken out of service.");
        // Here you would release resources, e.g., save the final count to a database.
    }
}

Key Takeaways

  • The Servlet Container manages the entire lifecycle of a servlet.
  • The lifecycle consists of three main phases marked by the init(), service(), and destroy() methods.
  • init() is called once for one-time initialization.
  • service() (and by extension doGet/doPost) is called for every request.
  • destroy() is called once for resource cleanup before the servlet is removed.
  • The container creates a single instance of a servlet and uses multiple threads to handle concurrent requests, which makes thread safety a critical concern for instance variables.

What's Next?

You now have a solid understanding of how a servlet lives and breathes within the container. But what if you want to perform an action on a request before it even reaches the servlet, or modify a response after the servlet has processed it? This is useful for tasks like logging, authentication, or data compression.

In the next lesson, "Implement a Servlet Filter to intercept and process requests," we'll explore Servlet Filters, which are powerful components that allow you to do exactly that.