namalzi

← All posts

Digging into Tomcat's Source 3: Lifecycle and Startup — What Happens After catalina.sh start

Tomcat · Published

We follow the source from catalina.sh start to a running server — Bootstrap building class loaders, Catalina turning server.xml into a tree of objects with the Digester, and every component being initialized and started by the same Lifecycle state machine. With a listener and small examples we check the order in which parents start their children, why the server still comes up when a connector fails, and what the shutdown port 8005 does.

What this post covers

This is the third post in the "Digging into Tomcat's Source" series. In part 2 we saw which class each element of server.xml — <Server>, <Service>, <Connector>, <Engine>, <Host>, <Context> — becomes, and how they are tied together as parents and children. This time we look at when, by whom and in what order those objects are created, started and stopped: from typing catalina.sh start until the "Server startup in [...] milliseconds" log line, and from catalina.sh stop until the process exits.

There are four questions to answer.

Item Value
Project Apache Tomcat
Version 12.0.0-M1 (main)
Commit 17aa1769b0 (2026-10-23)
Main classes startup/Bootstrap, startup/Catalina, tomcat/util/digester/Digester, Lifecycle, LifecycleState, util/LifecycleBase, core/StandardServer, core/StandardService, core/ContainerBase

All line numbers in this post refer to this commit. How to get the same source and build the Docker image for the examples is in part 1. Compared with the stable 11.0.x branch, the files at the heart of this post — Bootstrap, Catalina, Lifecycle, LifecycleState, LifecycleBase, StandardServer, StandardService and ContainerBase — are identical apart from a few comment lines, and in the other classes quoted here the parts this post covers have not changed either.

Catalina.java is 1,200 lines long and ContainerBase.java is over 1,200 too. This post picks only the main path that a first-time reader can follow. Everything left out is listed by name and location in "What this post leaves out" at the end.

Terms to know first

Bootstrap and Catalina are the two classes that start Tomcat. Bootstrap is a very small class with the main() method; all it does is build class loaders and bring up Catalina. Catalina does the real work — reading server.xml, starting the server, waiting for the shutdown command and stopping it. (The servlet container part of Tomcat as a whole is also called Catalina, which is easy to mix up; when this post writes Catalina in code style, it means this one class.)

CATALINA_HOME and CATALINA_BASE are two folders. CATALINA_HOME is where Tomcat is installed (bin/, lib/), and CATALINA_BASE is where this instance's configuration and web apps live (conf/, webapps/, logs/). If you don't set the latter, they are the same (Bootstrap.java lines 110–123). The examples in this post use a separate CATALINA_BASE=/tmp/base so they don't touch the installation.

The Digester is a tool that reads an XML file and creates and wires Java objects according to rules of the form "when an element at this path appears, do this". It started as an Apache Commons library; Tomcat keeps its own copy in the org.apache.tomcat.util.digester package. This is where server.xml becomes a tree of objects.

Lifecycle is an interface implemented by nearly every Tomcat component — Server, Service, Connector, Engine, Host, Context, Wrapper, valves, loaders and so on. It has four methods, init(), start(), stop() and destroy(), plus a current state (LifecycleState). Think of it as "one shared way to switch everything on and off".

LifecycleListener is an object that is notified (receives an event) every time a component's state changes. The <Listener className="..."/> you put in server.xml is exactly this. The VersionLoggerListener in the default server.xml (it prints the version at startup) is one of them.

The shutdown port is the 8005 in <Server port="8005" shutdown="SHUTDOWN">. A running Tomcat waits on this port for the text SHUTDOWN, and stops itself when it arrives. We'll see it in section 6.

1. From catalina.sh start to Bootstrap.main()

catalina.sh start is a shell script. After sorting out environment variables, it launches java like this.

  eval $_NOHUP "\"$_RUNJAVA\"" "\"$CATALINA_LOGGING_CONFIG\"" $LOGGING_MANAGER "$JAVA_OPTS" "$CATALINA_OPTS" \
    -classpath "\"$CLASSPATH\"" \
    -Dcatalina.base="\"$CATALINA_BASE\"" \
    -Dcatalina.home="\"$CATALINA_HOME\"" \
    -Djava.io.tmpdir="\"$CATALINA_TMPDIR\"" \
    org.apache.catalina.startup.Bootstrap "$@" start \
    >> "$CATALINA_OUT" 2>&1 "&"

bin/catalina.sh lines 416–422

Three things can be read here. The class being run is org.apache.catalina.startup.Bootstrap, and its last argument is start. Standard output and error are appended to catalina.out. And because of the trailing &, Java runs in the background, while the script immediately prints Tomcat started. and exits (line 428).

Only two jars are on the class path

What goes into CLASSPATH is just bin/bootstrap.jar (line 214) and bin/tomcat-juli.jar (lines 225–231). Neither lib/catalina.jar, Tomcat's main body, nor lib/servlet-api.jar, the servlet API, is there. Printing the command line of the running Java process in the example container, and the contents of bootstrap.jar, gives this (part of example 1; the whole example comes in section 4).

===== java command line: -classpath =====
-classpath
/opt/tomcat/bin/bootstrap.jar:/opt/tomcat/bin/tomcat-juli.jar
===== classes in bin/bootstrap.jar =====
org/apache/catalina/startup/Bootstrap.class
org/apache/catalina/startup/CatalinaProperties.class
org/apache/catalina/startup/ClassLoaderFactory$Repository.class
org/apache/catalina/startup/ClassLoaderFactory$RepositoryType.class
org/apache/catalina/startup/ClassLoaderFactory.class
org/apache/catalina/startup/Constants.class
org/apache/catalina/startup/SafeForkJoinWorkerThreadFactory$SafeForkJoinWorkerThread.class
org/apache/catalina/startup/SafeForkJoinWorkerThreadFactory.class
org/apache/catalina/startup/Tool.class
org/apache/catalina/webresources/war/Handler.class
org/apache/catalina/webresources/war/WarURLConnection.class
org/apache/tomcat/util/buf/UriUtil.class

bootstrap.jar holds only twelve classes, and Catalina is not one of them. The hundreds of other classes are loaded from lib/ by class loaders that Bootstrap builds itself.

Bootstrap.init() — build class loaders, then reach Catalina through reflection

main() first creates a Bootstrap object and calls its init() (lines 441–454). Here is init().

    public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isTraceEnabled()) {
            log.trace("Loading startup class");
        }
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();
        // ...
        String methodName = "setParentClassLoader";
        Class<?>[] paramTypes = new Class[1];
        paramTypes[0] = Class.forName("java.lang.ClassLoader");
        Object[] paramValues = new Object[1];
        paramValues[0] = sharedLoader;
        Method method = startupInstance.getClass().getMethod(methodName, paramTypes);
        method.invoke(startupInstance, paramValues);

        catalinaDaemon = startupInstance;
    }

java/org/apache/catalina/startup/Bootstrap.java lines 251–277

  1. initClassLoaders() builds three class loaders: common, server and shared (lines 142–156). Which folders each one looks at is decided by common.loader, server.loader and shared.loader in conf/catalina.properties. In the default configuration only common.loader has a value (things like ${catalina.base}/lib and ${catalina.home}/lib/*.jar); the other two are empty. When the value is empty, createClassLoader() doesn't build a new loader but returns the parent as it is (lines 161–164), so by default all three names point at the same common loader.
  2. catalinaLoader.loadClass(...) loads the Catalina class from that loader and creates an instance.
  3. The methods of that object are called through reflection (getMethod() + invoke()). That starts with setParentClassLoader(), and the later load(), start() and stop() calls all work the same way (lines 283–357).

Why not just write new Catalina()? Bootstrap was loaded by the JVM's default class loader (the application class loader), and catalina.jar is not on that loader's class path. If the Bootstrap code named the type Catalina directly, the JVM would look for Catalina in the loader that loaded Bootstrap and fail. So Bootstrap only holds Catalina as an Object and finds its methods by name (as strings). This way all of Tomcat's own classes can live under lib/ and be swapped through catalina.properties. How the three loaders relate in detail, and the extra loader each web app gets, is the subject of part 4.

The start command in main()

After init(), main() reads the last argument as the command. For start it does three things.

                case "start":
                    daemon.setAwait(true);
                    daemon.load(args);
                    daemon.start();
                    if (null == daemon.getServer()) {
                        System.exit(1);
                    }
                    break;

java/org/apache/catalina/startup/Bootstrap.java lines 479–486

setAwait(true) means "after starting everything, wait until a shutdown command arrives" (section 6). load() calls Catalina.load() (section 2) and start() calls Catalina.start() (sections 3 and 4). Other commands include stop (send the shutdown command, section 6) and configtest (stop after load()) (lines 469–500). Here is the whole flow as a picture first.

The flow from catalina.sh start to JVM exit. catalina.sh start puts only bootstrap.jar and tomcat-juli.jar on the class path and runs Bootstrap start. Bootstrap.init builds the common, server and shared class loaders and creates Catalina through reflection. Catalina.load uses the Digester to turn server.xml into a tree of objects, and Server.init initializes Service, Engine and Connector in turn. Catalina.start calls Server.start, which starts Engine, Host and Context and then the Connector; a JVM shutdown hook is registered and StandardServer.await waits for SHUTDOWN on port 8005. When the command arrives, Catalina.stop calls Server.stop and Server.destroy, main returns and the JVM exits. Grey boxes are the script and the command line, blue boxes are Java code. In the yellow box the main thread sits still for the whole life of the server.

Easy to get wrong: "Tomcat started." doesn't mean startup is finished

The Tomcat started. printed by catalina.sh start only means java was launched in the background. At that moment Java may not even have read server.xml yet. That is why the examples in this series follow catalina.sh start with a loop that retries curl until it succeeds. Even if startup fails, the script still prints Tomcat started., so failures have to be found in catalina.out. To run in the foreground, use catalina.sh run; it runs the same Bootstrap ... start, just without the & (lines 348–356).

2. Catalina.load() — server.xml into a tree of objects

Here is the first part of Catalina.load().

    public void load() {

        if (loaded) {
            return;
        }
        // No load retry after a failure
        loaded = true;

        long t1 = System.nanoTime();

        // Before digester - it may be needed
        initNaming();

        // Parse main server.xml
        parseServerXml(true);
        Server s = getServer();
        if (s == null) {
            return;
        }

        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

        // Stream redirection
        initStreams();

        // Start the new server
        try {
            getServer().init();

java/org/apache/catalina/startup/Catalina.java lines 820–849

  1. The loaded flag makes it run only once. Even after a failure it doesn't try again.
  2. initNaming() sets system properties for JNDI (the Java standard for looking up resources by names like java:comp/env).
  3. parseServerXml(true) reads server.xml and builds the Server object. This is the heart of this section.
  4. initStreams() swaps System.out and System.err for Tomcat's own streams (lines 1028–1032).
  5. getServer().init() initializes the components (sections 3 and 4). When it's done it logs Server initialization in [...] milliseconds (lines 865–868).

Digester rules — "when this path appears, do this"

parseServerXml() creates a Digester filled with rules by createStartDigester(), pushes the Catalina object itself onto the bottom of the stack, and then has it read the file.

                Digester digester = start ? createStartDigester() : createStopDigester();
                InputStream inputStream = resource.getInputStream();
                InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
                inputSource.setByteStream(inputStream);
                digester.push(this);

java/org/apache/catalina/startup/Catalina.java lines 721–725

The rules look like this. Only the ones for <Server> and the elements directly under it are shown.

        digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className");
        digester.addSetProperties("Server");
        digester.addSetNext("Server", "setServer", "org.apache.catalina.Server");

        digester.addObjectCreate("Server/GlobalNamingResources", "org.apache.catalina.deploy.NamingResourcesImpl");
        digester.addSetProperties("Server/GlobalNamingResources");
        digester.addSetNext("Server/GlobalNamingResources", "setGlobalNamingResources",
                "org.apache.catalina.deploy.NamingResourcesImpl");

        digester.addRule("Server/Listener", new ListenerCreateRule(null, "className"));
        digester.addSetProperties("Server/Listener");
        digester.addSetNext("Server/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");

        digester.addObjectCreate("Server/Service", "org.apache.catalina.core.StandardService", "className");
        digester.addSetProperties("Server/Service");
        digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service");

java/org/apache/catalina/startup/Catalina.java lines 513–528

Each element path ("Server/Service") gets a set of three rules.

Rule When What it does
ObjectCreate start tag loads the class, calls new and pushes the object. A className attribute replaces the class (ObjectCreateRule.java lines 87–107)
SetProperties start tag calls the setter with the same name as each attribute. port="8005" → setPort(8005) (SetPropertiesRule.java lines 83–135)
SetNext end tag passes the top object (the child) to a method of the object right below it (the parent). server.addService(service) (SetNextRule.java lines 72–96)

At an end tag the rules run in the reverse of the order they were added (Digester.java lines 1110–1127). So SetNext attaches the object to its parent first, and then ObjectCreate's end() pops it off the stack. The parent of <Server> is the Catalina object pushed at the bottom, so the final </Server> calls catalina.setServer(server) and the whole object tree hangs off Catalina. The rules for <Engine>, <Host> and <Context> are bundled as EngineRuleSet, HostRuleSet and ContextRuleSet (lines 589–594), and the <Host> rules include one that attaches HostConfig, which deploys web apps, as a listener (HostRuleSet.java lines 64–65).

How the Digester reads server.xml. Before parsing, the stack holds Catalina. At the Server start tag a StandardServer is created and pushed, and setters such as setPort and setShutdown are called. At the Service start tag a StandardService is created and pushed. At the Connector tags a Connector is created and service.addConnector is called. At the Engine and Host tags a StandardEngine and a StandardHost are created and the HostConfig listener is attached. At the Service end tag server.addService is called and the service is popped. At the Server end tag catalina.setServer is called. Catalina.server now holds the object tree, but nothing has been initialized yet and the state is NEW. Attaching to the parent (SetNext) happens at the end tag. When this step is over, every component is still in the NEW state.

Easy to get wrong: creating objects and starting them are separate

All the Digester does is new, setters and addXxx() calls. It doesn't open ports or read web apps yet. For example, </Host> calls engine.addChild(host), and ContainerBase.addChild() only calls start() on the child when the parent is already running (ContainerBase.java lines 581–584). Right now the Engine is NEW, so the host is just attached. Starting is done separately, by init() and start() in the next sections.

Example 2: breaking server.xml on purpose

I put three common mistakes into server.xml and ran catalina.sh configtest. configtest is the command where Bootstrap stops after load() (lines 490–496), which makes it handy for checking configuration. This example was run in a Docker container (Eclipse Temurin JDK 21) with Tomcat 12.0.0-M1-dev built as-is from the reference commit (17aa1769b0). Here is the first part of ex/u03-bad-server-xml/run.sh.

# Written for this post: break server.xml on purpose and see where Tomcat says what
# ... fresh(): copy the default conf/ into a clean CATALINA_BASE, show(): keep only the interesting log lines ...
echo "===== 1) typo in an attribute: connectionTimout (catalina.sh configtest) ====="
fresh
sed -i 's/connectionTimeout="20000"/connectionTimout="20000"/' /tmp/base/conf/server.xml
catalina.sh configtest > /tmp/base/logs/catalina.out 2>&1; echo "exit code: $?"
show 4
# ... 2) insert <Conector port="8081" /> before <Connector, 3) delete </Host> — same pattern ...

[Shortened from the example ex/u03-bad-server-xml/run.sh]

===== 1) typo in an attribute: connectionTimout (catalina.sh configtest) =====
exit code: 0
WARNING: Match [Server/Service/Connector] failed to set property [connectionTimout] to [20000]
INFO: Server initialization in [836] milliseconds
===== 2) typo in an element: <Conector> (catalina.sh configtest) =====
exit code: 0
WARNING: No rules found matching [Server/Service/Conector]
INFO: Server initialization in [984] milliseconds
===== 3) broken XML: </Host> removed (catalina.sh configtest) =====
exit code: 1
SEVERE: Parse fatal error at line [152] column [7]
WARNING: Unable to load server configuration from [/tmp/base/conf/server.xml]

The millisecond values differ on every run.

It's worth remembering the two phrases in those warnings (Match [...] failed to set property, No rules found matching). Both only warn, and startup goes on. The check that createStartDigester() turns on (setRulesValidation(true), line 495) also stops at logging a warning, so after editing server.xml you have to look for the WARNING lines in the startup log yourself.

3. Lifecycle — the state machine every component follows

Before following getServer().init(), let's see what that init() looks like. There are twelve states in the LifecycleState enum.

State Available (available) Event fired on entering this state
NEW no none
INITIALIZING no before_init
INITIALIZED no after_init
STARTING_PREP no before_start
STARTING yes start
STARTED yes after_start
STOPPING_PREP yes before_stop
STOPPING no stop
STOPPED no after_stop
DESTROYING no before_destroy
DESTROYED no after_destroy
FAILED no none

java/org/apache/catalina/LifecycleState.java lines 23–71

Each state carries two values. The first is available — may the component be used in this state? — and only STARTING, STARTED and STOPPING_PREP are true. The second is the name of the event to fire when the component enters this state. Entering STARTING_PREP fires before_start, entering STARTING fires start. NEW and FAILED have no event.

Lifecycle state transitions. Calling init() on NEW goes through INITIALIZING (before_init) to INITIALIZED (after_init). Calling start() goes to STARTING_PREP (before_start), to STARTING (start) inside startInternal(), and then to STARTED (after_start). Calling stop() goes to STOPPING_PREP (before_stop), to STOPPING (stop) inside stopInternal(), and to STOPPED (after_stop). From STOPPED start() can be called again, and destroy() goes through DESTROYING (before_destroy) to DESTROYED (after_destroy). When an xxxInternal() throws, the state becomes FAILED, and it moves to STOPPING when stop() cleans it up. The second line in each box is the event fired on entering that state. The comment in Lifecycle.java lines 26–76 draws a more detailed version in plain text.

LifecycleBase — the parent class owns the rules, subclasses do the work

So that every component doesn't have to implement these rules, an abstract class called LifecycleBase implements the four methods as final. Here is the middle of start().

        if (state.equals(LifecycleState.NEW)) {
            init();
        } else if (state.equals(LifecycleState.FAILED)) {
            stop();
        } else if (!state.equals(LifecycleState.INITIALIZED) && !state.equals(LifecycleState.STOPPED)) {
            invalidTransition(BEFORE_START_EVENT);
        }

        try {
            setStateInternal(LifecycleState.STARTING_PREP, null, false);
            startInternal();
            if (state.equals(LifecycleState.FAILED)) {
                // This is a 'controlled' failure. The component put itself into the
                // FAILED state so call stop() to complete the clean-up.
                stop();
            } else if (!state.equals(LifecycleState.STARTING)) {
                // Shouldn't be necessary but acts as a check that subclasses are
                // doing what they are supposed to.
                invalidTransition(AFTER_START_EVENT);
            } else {
                setStateInternal(LifecycleState.STARTED, null, false);
            }

java/org/apache/catalina/util/LifecycleBase.java lines 160–181

This method follows the template method pattern. LifecycleBase.start() fixes the order and the checks, and the subclass fills in the actual work with startInternal(). init() pairs with initInternal(), stop() with stopInternal() and destroy() with destroyInternal(). StandardServer, StandardService, Connector and ContainerBase all extend LifecycleBase (with LifecycleMBeanBase, which handles JMX registration, in between) and implement only the xxxInternal() methods.

There are four things to read here.

  1. Calling start() on a NEW component runs init() first (lines 160–161). This becomes important in section 4.
  2. Calling start() again on a component that is already running (STARTING_PREP, STARTING, STARTED) just logs at INFO and returns (lines 147–158).
  3. Moving the state to STARTING is the subclass's job. If the state isn't STARTING when startInternal() returns, it counts as an invalid transition (lines 175–178).
  4. There are two kinds of failure. If startInternal() throws, handleSubClassException() sets the state to FAILED and rethrows (lines 182–186, 406–417). If the subclass sets itself to FAILED and returns quietly, stop() cleans up and there is no exception (lines 171–174).

Events are fired at the end of setStateInternal(), in just these lines.

        this.state = state;
        String lifecycleEvent = state.getLifecycleEvent();
        if (lifecycleEvent != null) {
            fireLifecycleEvent(lifecycleEvent, data);
        }

java/org/apache/catalina/util/LifecycleBase.java lines 393–397

The state is changed first, then the event is sent. So a listener that calls getState() while handling the event already sees the new state. fireLifecycleEvent() walks the registered listeners in order and calls lifecycleEvent() on each (lines 112–117). The listener list is a CopyOnWriteArrayList (line 52).

Example 3: poking the state machine with a single LifecycleBase

Without starting a server, I wrote a small component that extends LifecycleBase directly and poked the rules one by one. It uses just one class from catalina.jar.

// Written for this post (not Tomcat source)
// ... imports omitted ...
    /** A component that prints a line whenever an xxxInternal() is called or an event arrives */
    static class Part extends LifecycleBase {
        // ... fields and constructor: registers one listener that prints "event <name> (state=<state>)" ...

        @Override
        protected void startInternal() throws LifecycleException {
            System.out.println("    startInternal()");
            if ("throw".equals(failMode)) {
                throw new LifecycleException("boom in startInternal");
            }
            if ("setFailed".equals(failMode)) {
                setState(LifecycleState.FAILED); // report the failure itself (a "controlled" failure)
                return;
            }
            setState(LifecycleState.STARTING); // the subclass's job — this is what fires the start event
        }

        @Override
        protected void stopInternal() throws LifecycleException {
            System.out.println("    stopInternal()");
            setState(LifecycleState.STOPPING);
        }
        // ... initInternal() and destroyInternal() just print their names ...
    }

[Shortened from the example ex/u03-state-machine/StateMachine.java]

main() uses four components (a–d) to call the methods shown on the -- lines of the output below, prints any exception on a !! line and the resulting state on a => line. It was run with java -cp "$CATALINA_HOME/lib/*:$CATALINA_HOME/bin/*" StateMachine.java (JDK 21 can run a source file directly). Lines starting with [log] are Tomcat's own log messages.

===== A: start() on a NEW part, then stop() and destroy() =====
-- a.start()
    event before_init (state=INITIALIZING)
    initInternal()
    event after_init (state=INITIALIZED)
    event before_start (state=STARTING_PREP)
    startInternal()
    event start (state=STARTING)
    event after_start (state=STARTED)
    => state STARTED
-- a.start() again
    [log] INFO: The start() method was called on component [Part[a]] after start() had already been called. The second call will be ignored.
    => state STARTED
-- a.stop()
    event before_stop (state=STOPPING_PREP)
    stopInternal()
    event stop (state=STOPPING)
    event after_stop (state=STOPPED)
    => state STOPPED
-- a.destroy()
    event before_destroy (state=DESTROYING)
    destroyInternal()
    event after_destroy (state=DESTROYED)
    => state DESTROYED
===== B: invalid transition =====
-- b.init()
    event before_init (state=INITIALIZING)
    initInternal()
    event after_init (state=INITIALIZED)
    => state INITIALIZED
-- b.init() again
    !! LifecycleException: An invalid Lifecycle transition was attempted ([before_init]) for component [Part[b]] in state [INITIALIZED]
    => state INITIALIZED
-- b.stop() while INITIALIZED
    => state INITIALIZED
===== C: startInternal() throws =====
-- c.start()
    event before_init (state=INITIALIZING)
    initInternal()
    event after_init (state=INITIALIZED)
    event before_start (state=STARTING_PREP)
    startInternal()
    !! LifecycleException: boom in startInternal
    => state FAILED
-- c.destroy()
    event before_stop (state=FAILED)
    stopInternal()
    event stop (state=STOPPING)
    event after_stop (state=STOPPED)
    event before_destroy (state=DESTROYING)
    destroyInternal()
    event after_destroy (state=DESTROYED)
    => state DESTROYED
===== D: startInternal() sets FAILED itself =====
-- d.start()
    event before_init (state=INITIALIZING)
    initInternal()
    event after_init (state=INITIALIZED)
    event before_start (state=STARTING_PREP)
    startInternal()
    event before_stop (state=FAILED)
    stopInternal()
    event stop (state=STOPPING)
    event after_stop (state=STOPPED)
    => state STOPPED

Easy to get wrong: LifecycleBase isn't the one firing the start event

before_start and after_start come from LifecycleBase.start() itself, but the start event in the middle goes out when the subclass calls setState(LifecycleState.STARTING) inside startInternal(). Where that call sits differs from class to class. That is what decides the order of parent and child events in the next section.

4. The order in which parents start their children

Now let's follow Server.init() and Server.start(). The rule is simple: a parent's xxxInternal() calls its children's xxx(). The server calls the services, a service calls the engine and the connectors, the engine calls the hosts, and so on down the tree.

Here is the core of StandardServer.startInternal().

    protected void startInternal() throws LifecycleException {

        fireLifecycleEvent(CONFIGURE_START_EVENT, null);
        setState(LifecycleState.STARTING);
        // ...
        globalNamingResources.start();

        // Start our defined Services
        for (Service service : findServices()) {
            service.start();
        }
        // ...
    }

java/org/apache/catalina/core/StandardServer.java lines 863–885

In StandardService.startInternal() the order stands out.

    protected void startInternal() throws LifecycleException {

        if (log.isInfoEnabled()) {
            log.info(sm.getString("standardService.start.name", this.name));
        }
        setState(LifecycleState.STARTING);

        // Start our defined Container first
        if (engine != null) {
            engine.start();
        }

        for (Executor executor : findExecutors()) {
            executor.start();
        }

        mapperListener.start();

        // Start our defined Connectors second
        for (Connector connector : findConnectors()) {
            // If it has already failed, don't try and start it
            if (connector.getState() != LifecycleState.FAILED) {
                connector.start();
            }
        }
    }

java/org/apache/catalina/core/StandardService.java lines 424–449

Just as the comments say, the container (Engine) first, the connectors second. The entrance that takes requests (the connector) opens only after the side that handles them (the engine and the web apps under it) is fully ready. The other way around, requests arriving while web apps were still being deployed would get a 404. Stopping goes in reverse: connectors are paused first (pause()) to block new requests, and then the engine is stopped (lines 459–512). The mapperListener in the middle fills in the map that decides which web app a request address goes to; it's covered in part 7.

ContainerBase.startInternal(), shared by Engine, Host, Context and Wrapper, is different.

        // Start our child containers, if any
        Container[] children = findChildren();
        List<Future<Void>> results = new ArrayList<>(children.length);
        for (Container child : children) {
            results.add(startStopExecutor.submit(new StartChild(child)));
        }
        // ... wait for every Future; if any failed, collect them into a LifecycleException ...

        // Start the Valves in our pipeline (including the basic), if any
        if (pipeline instanceof Lifecycle) {
            ((Lifecycle) pipeline).start();
        }

        setState(LifecycleState.STARTING);

java/org/apache/catalina/core/ContainerBase.java lines 728–759

Two things differ from StandardService.

  1. Children are started by handing them to an executor. Starting one child is a small task called StartChild (lines 1200–1207), and the executor depends on startStopThreads. The default is 1 (line 244), and with 1 it uses InlineExecutorService, a fake executor that runs the task right there, on the same thread (lines 689–701). Raise it, as in <Host startStopThreads="4">, and several web apps are started at once on the server's utility thread pool.
  2. setState(STARTING) is called after all the children have started. The server and the service fire their own start event first and then call their children; a container fires its start event only after its children are up.

The start order. Server.start and Service.start enter STARTING first and fire their start event. Then the Service calls ① Engine.start, and the Engine calls Host.start; the Host is still NEW, so init runs first. The moment the Host enters STARTING, HostConfig deploys webapps and Context.start is called. Only after its children are up does the Engine enter STARTING and fire its start event. After that come ② the Executor and the MapperListener, and finally ③ Connector.start, at which point 8080 starts taking requests. The circled numbers are the order inside StandardService.startInternal(). Web app deployment happens in the yellow box.

Example 1: a listener on every element, printing the event order

To check that this really is the order, I hung the same listener on every element of server.xml and printed the events in the order they arrived.

// Written for this post (not Tomcat source)
package lab;
// ... imports omitted ...

/**
 * Hang this on several server.xml elements with <Listener className="lab.EventLogger"/>
 * and print one line per Lifecycle event, in the order received.
 */
public class EventLogger implements LifecycleListener {

    // a sequence number shared by every listener instance — to see the global order across elements
    private static final AtomicInteger SEQ = new AtomicInteger();

    @Override
    public void lifecycleEvent(LifecycleEvent event) {
        // the Server sends a periodic event every 10 seconds, so skip it
        if (Lifecycle.PERIODIC_EVENT.equals(event.getType())) {
            return;
        }
        Lifecycle source = event.getLifecycle();
        System.out.printf("EVT %02d %-22s %-15s state=%-13s thread=%s%n",
                SEQ.incrementAndGet(), name(source), event.getType(),
                source.getState(), Thread.currentThread().getName());

        // print the call stack once, when the Host is being initialized — who initializes the Host?
        if (source instanceof org.apache.catalina.Host && Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {
            new Throwable("STACK at Host before_init").printStackTrace(System.out);
        }
    }
    // ... name(): short names such as "Server", "Service[Catalina]", "Connector[8080]", "Host[localhost]" ...
}

[Shortened from the example ex/u03-lifecycle-events/EventLogger.java]

Instead of the default server.xml I used a small one with just the listeners (so the default file's listeners such as VersionLoggerListener are absent).

<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="lab.EventLogger" />
  <Service name="Catalina">
    <Listener className="lab.EventLogger" />
    <Connector port="8080" protocol="HTTP/1.1">
      <Listener className="lab.EventLogger" />
    </Connector>
    <Engine name="Catalina" defaultHost="localhost">
      <Listener className="lab.EventLogger" />
      <Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="false">
        <Listener className="lab.EventLogger" />
      </Host>
    </Engine>
  </Service>
</Server>

[The example ex/u03-lifecycle-events/server.xml without its first two lines (the XML declaration and a comment)]

The compiled class goes to $CATALINA_BASE/lib/lab/EventLogger.class. The common loader from section 1 looks at the ${catalina.base}/lib folder, so the Digester can find the class. The web app (Context) isn't written in server.xml; a single webapps/app/index.html gets auto-deployed, and the same listener is also added to conf/context.xml, which applies to every web app. I started it with catalina.sh start, checked that curl got hello back, and picked the EVT lines out of catalina.out. This example also ran on the same Docker image. First, the init phase.

EVT 01 Server                 before_init     state=INITIALIZING  thread=main
EVT 02 Service[Catalina]      before_init     state=INITIALIZING  thread=main
EVT 03 Engine[Catalina]       before_init     state=INITIALIZING  thread=main
EVT 04 Engine[Catalina]       after_init      state=INITIALIZED   thread=main
EVT 05 Connector[8080]        before_init     state=INITIALIZING  thread=main
EVT 06 Connector[8080]        after_init      state=INITIALIZED   thread=main
EVT 07 Service[Catalina]      after_init      state=INITIALIZED   thread=main
EVT 08 Server                 after_init      state=INITIALIZED   thread=main
24-Oct-2026 13:47:28.233 INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [1980] milliseconds

init went down only as far as Server → Service → Engine → Connector. There is no Host. StandardService.initInternal() calls init() on the engine and the connectors (lines 522–545), but the engine's initInternal() only prepares the Realm (the component that holds user authentication data) and calls its parent class's initInternal() (JMX registration); it doesn't init() its children (StandardEngine.java lines 184–189). ContainerBase has no initInternal() of its own. The connector being part of the init phase also means its port opens in this phase (section 5). Next, the start phase.

EVT 09 Server                 before_start    state=STARTING_PREP thread=main
EVT 10 Server                 configure_start state=STARTING_PREP thread=main
EVT 11 Server                 start           state=STARTING      thread=main
EVT 12 Service[Catalina]      before_start    state=STARTING_PREP thread=main
24-Oct-2026 13:47:28.263 INFO [main] org.apache.catalina.core.StandardService.startInternal Starting service [Catalina]
EVT 13 Service[Catalina]      start           state=STARTING      thread=main
EVT 14 Engine[Catalina]       before_start    state=STARTING_PREP thread=main
24-Oct-2026 13:47:28.265 INFO [main] org.apache.catalina.core.StandardEngine.startInternal Starting Servlet engine: [Apache Tomcat/12.0.0-M1-dev]
EVT 15 Host[localhost]        before_init     state=INITIALIZING  thread=main
java.lang.Throwable: STACK at Host before_init
	at lab.EventLogger.lifecycleEvent(EventLogger.java:36)
	at org.apache.catalina.util.LifecycleBase.fireLifecycleEvent(LifecycleBase.java:115)
	at org.apache.catalina.util.LifecycleBase.setStateInternal(LifecycleBase.java:396)
	at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:127)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:161)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1204)
	at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1200)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:95)
	at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145)
	at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:732)
	at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:201)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:170)
	at org.apache.catalina.core.StandardService.startInternal(StandardService.java:433)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:170)
	at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:878)
	at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:170)
	at org.apache.catalina.startup.Catalina.start(Catalina.java:921)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
	at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:482)
EVT 16 Host[localhost]        after_init      state=INITIALIZED   thread=main
EVT 17 Host[localhost]        before_start    state=STARTING_PREP thread=main
24-Oct-2026 13:47:28.315 INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/tmp/base/webapps/app]
EVT 18 Context[/app]          before_start    state=STARTING_PREP thread=main
EVT 19 Context[/app]          configure_start state=STARTING_PREP thread=main
EVT 20 Context[/app]          start           state=STARTING      thread=main
EVT 21 Context[/app]          after_start     state=STARTED       thread=main
EVT 22 Host[localhost]        start           state=STARTING      thread=main
EVT 23 Host[localhost]        after_start     state=STARTED       thread=main
EVT 24 Engine[Catalina]       start           state=STARTING      thread=main
EVT 25 Engine[Catalina]       after_start     state=STARTED       thread=main
EVT 26 Connector[8080]        before_start    state=STARTING_PREP thread=main
EVT 27 Connector[8080]        start           state=STARTING      thread=main
EVT 28 Connector[8080]        after_start     state=STARTED       thread=main
EVT 29 Service[Catalina]      after_start     state=STARTED       thread=main
EVT 30 Server                 after_start     state=STARTED       thread=main
24-Oct-2026 13:47:29.267 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [1031] milliseconds

Timestamps and milliseconds differ on every run. There are five things to read.

Easy to get wrong: a listener in context.xml misses the init events

In the output, the first event for Context[/app] is before_start (event 18). There is no before_init or after_init. This listener was put not in server.xml but in conf/context.xml, and context.xml is read by ContextConfig — when it receives the Context's after_init event (ContextConfig.java line 310, lines 997–1013). The listener is attached while after_init is already being handed out, so it misses that event (the listener list is a CopyOnWriteArrayList, so a new listener doesn't join the list that is already being walked). If you need to do something when a web app initializes, a <Listener> in context.xml won't do it.

5. When things fail — the server comes up even when a connector fails

As example C in section 3 showed, when an xxxInternal() throws, LifecycleBase rethrows the exception up to the parent. But when a setting called throwOnFailure is false, it doesn't rethrow and only logs (lines 406–417). The default is true (line 61), but Connector alone sets this value from a system property in its constructor.

        // Default for Connector depends on this system property
        setThrowOnFailure(Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"));

java/org/apache/catalina/connector/Connector.java lines 118–119

Without the property it's false. When a connector fails in init, the exception doesn't climb to the service; only that connector is left FAILED. And StandardService.startInternal() from section 4 skips connectors that are FAILED (line 445).

Example 2 (continued): two connectors on the same port

In the second half of example 2 I added another 8080 connector to server.xml so the ports clash. I started it once as is, and once with CATALINA_OPTS="-Dorg.apache.catalina.startup.EXIT_ON_INIT_FAILURE=true".

===== 4) two Connectors on port 8080 (catalina.sh start) =====
curl http://127.0.0.1:8080/ -> HTTP 404
SEVERE [main] org.apache.catalina.util.LifecycleBase.handleSubClassException Failed to initialize component [Connector["http-nio-8080"]]
	org.apache.catalina.LifecycleException: Protocol handler initialization failed
	Caused by: java.net.BindException: Address already in use
INFO [main] org.apache.catalina.startup.Catalina.load Server initialization in [1789] milliseconds
INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [138] milliseconds
===== 5) the same, with -Dorg.apache.catalina.startup.EXIT_ON_INIT_FAILURE=true (catalina.sh run) =====
exit code: 1
SEVERE [main] org.apache.catalina.startup.Bootstrap.main Error running command
	java.lang.Error: org.apache.catalina.LifecycleException: Protocol handler initialization failed
	Caused by: org.apache.catalina.LifecycleException: Protocol handler initialization failed
	Caused by: java.net.BindException: Address already in use
            if (throwOnInitFailure || await) {
                throw new Error(e);
            } else {
                log.error(sm.getString("catalina.initError"), e);
            }

java/org/apache/catalina/startup/Catalina.java lines 858–862

The comment just above (lines 851–857) gives the reason. Calling start() on a server in the FAILED state makes LifecycleBase.start() call stop() first, which makes await() return immediately, so the server would stop right after starting. If the plan is to wait (await), it's better to end here.

How far a connector's init failure travels. When init() of the second Connector hits a BindException while opening 8080, LifecycleBase.handleSubClassException() sets the connector to FAILED and checks the connector's throwOnFailure value (the EXIT_ON_INIT_FAILURE system property). If it is false because the property is not set, as in (4) of example 2, only a SEVERE line is logged, StandardService.startInternal() skips the FAILED connector, "Server startup in [138] milliseconds" is printed, and the first connector answers curl with 404. If it is true, as in (5), a LifecycleException is thrown, the Service and Server (which default to true) rethrow it upward, Catalina.load() turns it into an Error because throwOnInitFailure or await is set, and Bootstrap.main() logs "Error running command" and ends with exit code 1. One setting sends the same BindException down two different paths. The green path is the real output of (4) in example 2, the red path that of (5).

Easy to get wrong: "Server startup" doesn't mean everything came up

As in (4), Server startup in [...] is printed even when a connector fails. That's how you end up in production with one port that never opened while the server looks up. To turn that into a startup failure, set the EXIT_ON_INIT_FAILURE system property. The refresh() in Spring part 7 takes the opposite stance: its principle is "either every bean is created or none is", and if anything fails it tears down what it built and throws. Tomcat lets each component choose, through throwOnFailure, how far up a failure travels, and for connectors it chose "come up anyway" as the default.

6. await() and the shutdown port 8005

Here is the end of Catalina.start().

        // Register shutdown hook
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);
            // ... turn off JULI logging's shutdown hook (so logging doesn't close before the server) ...
        }

        if (await) {
            await();
            stop();
        }
    }

java/org/apache/catalina/startup/Catalina.java lines 942–962

As soon as await() returns, it's stop(). In other words, the main thread spends the entire life of the server inside await(). Requests are handled by the connector's threads (part 5); main only waits for the shutdown command. await() calls StandardServer.await().

        // Set up a server socket to wait on
        try {
            awaitSocket = new ServerSocket(getPortWithOffset(), 1, InetAddress.getByName(address));

java/org/apache/catalina/core/StandardServer.java lines 535–537

The port is <Server port> (default 8005, line 119), and the address is <Server address>, whose default is "localhost" (line 126). The shutdown port can only be reached from the same machine. After that, a loop accepts one connection (line 561), reads up to 1024 bytes until a control character appears (lines 580–602), and compares them with the configured string.

                // Decode the received bytes as UTF-8, matching the encoding
                // used by the stop command
                String command = commandBytes.toString(StandardCharsets.UTF_8);

                // Match against our command string
                boolean match = command.equals(shutdown);
                if (match) {
                    log.info(sm.getString("standardServer.shutdownViaPort"));
                    break;
                } else {
                    log.warn(sm.getString("standardServer.invalidShutdownCommand", command));
                }

java/org/apache/catalina/core/StandardServer.java lines 614–625

On a match it breaks out of the loop and await() returns; otherwise it logs a warning and waits for the next connection.

The sending side: catalina.sh stop

catalina.sh stop launches one more JVM that runs Bootstrap ... stop (lines 466–471). In that JVM, Catalina.stopServer() reads server.xml again, but with a Digester that only has rules for the <Server> element (createStopDigester(), lines 639–652), just to learn the port and the command string, and then connects to that port and sends the command.

            try (Socket socket = new Socket()) {
                socket.connect(new InetSocketAddress(s.getAddress(), s.getPortWithOffset()),
                        SHUTDOWN_CONNECT_TIMEOUT_MS);
                OutputStream stream = socket.getOutputStream();
                // Send the shutdown command as UTF-8 bytes. Writing the
                // characters one at a time with OutputStream.write(int) would
                // silently truncate any character above U+00FF and the server
                // would then never match the configured shutdown string.
                stream.write(s.getShutdown().getBytes(StandardCharsets.UTF_8));
                stream.flush();

java/org/apache/catalina/startup/Catalina.java lines 790–799

kill -TERM takes a different road

On kill -TERM (or Ctrl+C) the JVM runs its registered shutdown hooks (threads the JVM runs just before it exits). CatalinaShutdownHook calls Catalina.stop() (lines 1155–1172), and at the very end of that, StandardServer.stopInternal() closes the 8005 socket with stopAwait() (line 936, lines 489–509). The accept() on the main thread then wakes up with an exception and await() returns, and the stop() that main calls does nothing because the state is already somewhere between STOPPING_PREP and DESTROYED (Catalina.java lines 990–997).

The two ways to stop. On the left is catalina.sh stop. A new JVM runs Bootstrap stop, Catalina.stopServer reads only the Server element of server.xml and sends the bytes SHUTDOWN to localhost:8005. On the main thread of the running JVM, StandardServer.await compares the command; if it doesn't match it only warns and waits again, and if it matches await returns and the next line of Catalina.start, stop, calls Server.stop and destroy. On the right is kill -TERM. On the JVM shutdown hook thread, CatalinaShutdownHook calls Catalina.stop, and stopAwait in StandardServer.stopInternal closes the 8005 socket so that await returns. On the right-hand road the shutdown hook thread has already stopped the server, so main's stop() has nothing left to do.

Example 1 (continued): a wrong command, the right command, and kill

After startup, example 1 sends HELLO to 8005 with bash's /dev/tcp, stops the server with catalina.sh stop, then starts it again and stops it with kill -TERM.

# 3) send a wrong command to the shutdown port
echo "===== send 'HELLO' to port 8005 ====="
exec 3<>/dev/tcp/127.0.0.1/8005; printf 'HELLO' >&3; exec 3>&-
sleep 1

# 4) normal shutdown: catalina.sh stop sends SHUTDOWN to 8005
catalina.sh stop >/dev/null 2>&1
# ... 5) start again and kill -TERM $(pgrep -f Bootstrap) ...

[Shortened from the example ex/u03-lifecycle-events/run.sh]

24-Oct-2026 13:47:29.471 WARNING [main] org.apache.catalina.core.StandardServer.await Invalid shutdown command [HELLO] received
24-Oct-2026 13:47:31.724 INFO [main] org.apache.catalina.core.StandardServer.await A valid shutdown command was received via the shutdown port. Stopping the Server instance.
EVT 31 Server                 before_stop     state=STOPPING_PREP thread=main
EVT 32 Server                 stop            state=STOPPING      thread=main
EVT 33 Server                 configure_stop  state=STOPPING      thread=main
EVT 34 Service[Catalina]      before_stop     state=STOPPING_PREP thread=main
24-Oct-2026 13:47:31.781 INFO [main] org.apache.catalina.core.StandardService.stopInternal Stopping service [Catalina]
EVT 35 Service[Catalina]      stop            state=STOPPING      thread=main
EVT 36 Engine[Catalina]       before_stop     state=STOPPING_PREP thread=main
EVT 37 Engine[Catalina]       stop            state=STOPPING      thread=main
EVT 38 Host[localhost]        before_stop     state=STOPPING_PREP thread=main
EVT 39 Host[localhost]        stop            state=STOPPING      thread=main
EVT 40 Context[/app]          before_stop     state=STOPPING_PREP thread=main
EVT 41 Context[/app]          stop            state=STOPPING      thread=main
EVT 42 Context[/app]          configure_stop  state=STOPPING      thread=main
EVT 43 Context[/app]          after_stop      state=STOPPED       thread=main
EVT 44 Host[localhost]        after_stop      state=STOPPED       thread=main
EVT 45 Engine[Catalina]       after_stop      state=STOPPED       thread=main
EVT 46 Connector[8080]        before_stop     state=STOPPING_PREP thread=main
EVT 47 Connector[8080]        stop            state=STOPPING      thread=main
EVT 48 Connector[8080]        after_stop      state=STOPPED       thread=main
EVT 49 Service[Catalina]      after_stop      state=STOPPED       thread=main
EVT 50 Server                 after_stop      state=STOPPED       thread=main
EVT 51 Server                 before_destroy  state=DESTROYING    thread=main
EVT 52 Service[Catalina]      before_destroy  state=DESTROYING    thread=main
EVT 53 Connector[8080]        before_destroy  state=DESTROYING    thread=main
EVT 54 Connector[8080]        after_destroy   state=DESTROYED     thread=main
EVT 55 Engine[Catalina]       before_destroy  state=DESTROYING    thread=main
EVT 56 Host[localhost]        before_destroy  state=DESTROYING    thread=main
EVT 57 Context[/app]          before_destroy  state=DESTROYING    thread=main
EVT 58 Context[/app]          after_destroy   state=DESTROYED     thread=main
EVT 59 Host[localhost]        after_destroy   state=DESTROYED     thread=main
EVT 60 Engine[Catalina]       after_destroy   state=DESTROYED     thread=main
EVT 61 Service[Catalina]      after_destroy   state=DESTROYED     thread=main
EVT 62 Server                 after_destroy   state=DESTROYED     thread=main
===== after kill -TERM: Server/Service events while stopping =====
EVT 31 Server                 before_stop     state=STOPPING_PREP thread=Thread-1
EVT 32 Server                 stop            state=STOPPING      thread=Thread-1
EVT 33 Server                 configure_stop  state=STOPPING      thread=Thread-1
EVT 34 Service[Catalina]      before_stop     state=STOPPING_PREP thread=Thread-1
EVT 35 Service[Catalina]      stop            state=STOPPING      thread=Thread-1
EVT 49 Service[Catalina]      after_stop      state=STOPPED       thread=Thread-1
EVT 50 Server                 after_stop      state=STOPPED       thread=Thread-1
EVT 51 Server                 before_destroy  state=DESTROYING    thread=Thread-1
EVT 52 Service[Catalina]      before_destroy  state=DESTROYING    thread=Thread-1
EVT 61 Service[Catalina]      after_destroy   state=DESTROYED     thread=Thread-1
EVT 62 Server                 after_destroy   state=DESTROYED     thread=Thread-1

Timestamps differ on every run. The last group is from the second run, with only the server and service lines picked out.

Easy to get wrong: embedded Tomcat has no shutdown port

With port="-1", await() opens no socket and just wakes every 10 seconds to check the stopAwait flag (lines 519–533). The embedded Tomcat that Spring Boot uses sets this value to -1 when Tomcat.getServer() creates the Server (Tomcat.java line 641). That's why embedded Tomcat shows no server.xml, no Bootstrap and no 8005. Even so, Server, Service, Connector and the containers are started and stopped through the same Lifecycle. The embedded API is covered in part 16.

What this post leaves out

Summary

Concept In one line
catalina.sh start puts only bootstrap.jar and tomcat-juli.jar on the class path and launches Bootstrap ... start in the background. Tomcat started. doesn't mean startup is done
Bootstrap builds the common, server and shared loaders, loads Catalina from them and calls it through reflection
Catalina.load() runs once. The Digester turns server.xml into an object tree, then Server.init()
Digester per path: ObjectCreate (start tag), SetProperties, SetNext (end tag). Unknown attributes and elements only warn
LifecycleState twelve states. Entering a state fires the event attached to it
LifecycleBase final methods own the transition rules, xxxInternal() does the work. start() on NEW runs init() first
Start order Server → Service → Engine → Host → Context, and the Connector last. Hosts are initialized when their parent starts
Event order server and service fire start before their children, containers after their children
Web app deployment done by HostConfig during the Host's start event
Failure throwOnFailure. Connectors default to false, so the server comes up anyway. With EXIT_ON_INIT_FAILURE=true it exits
await() the main thread waits for SHUTDOWN on localhost:8005. When it returns, stop() and destroy()
Two ways to stop catalina.sh stop sends the command over a socket; on kill -TERM the shutdown hook calls stop() and closes the socket

What's next

This post only said that Bootstrap builds the common, server and shared class loaders. Tomcat builds one more class loader per web app, and that loader deliberately turns Java's default rule (ask the parent first) around. Why two web apps can use different classes with the same name, and why memory leaks when a web app is redeployed, continue in part 4: Class Loaders — Each Web App Sees Its Own Classes.

Compared with the earlier series: as we saw in part 5 of the httpd series, httpd reads its configuration twice inside main(), and handles a restart as "one more turn of a loop that rereads the configuration from scratch". Tomcat's Catalina.load() reads server.xml only once; after that, the components of the object tree are started and stopped by moving between Lifecycle states. For the stop signal, httpd uses Unix signals while Tomcat uses a TCP connection to port 8005. And unlike Spring's refresh(), which writes the startup order down as twelve steps in one method, Tomcat's order isn't written in one place; it's spread across each parent's startInternal(). That's why hanging a listener and printing, as in example 1, is the quickest way to check the order. Notifying listeners when the state changes resembles the way Spring plugs callbacks and post-processors into the bean lifecycle (Spring part 6).

More posts