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.
- What is the first Java code that runs after
catalina.sh start, and why does it callCatalinathrough reflection? - How does
server.xmlbecome a tree of objects, and what do you get — and where — when it has a typo? - What does the
Lifecyclestate machine that every component follows look like, and in what order do parents start their children? - What does the shutdown port 8005 wait for, and how is
catalina.sh stopdifferent fromkill?
| 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 "&"
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
initClassLoaders()builds three class loaders:common,serverandshared(lines 142–156). Which folders each one looks at is decided bycommon.loader,server.loaderandshared.loaderinconf/catalina.properties. In the default configuration onlycommon.loaderhas a value (things like${catalina.base}/liband${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 samecommonloader.catalinaLoader.loadClass(...)loads theCatalinaclass from that loader and creates an instance.- The methods of that object are called through reflection (
getMethod()+invoke()). That starts withsetParentClassLoader(), and the laterload(),start()andstop()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.
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
- The
loadedflag makes it run only once. Even after a failure it doesn't try again. initNaming()sets system properties for JNDI (the Java standard for looking up resources by names likejava:comp/env).parseServerXml(true)readsserver.xmland builds theServerobject. This is the heart of this section.initStreams()swapsSystem.outandSystem.errfor Tomcat's own streams (lines 1028–1032).getServer().init()initializes the components (sections 3 and 4). When it's done it logsServer 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).
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.
- A typo in an attribute name (1) produces a single warning, and the exit code is 0. When
SetPropertiesRulecan't find a setter, it only logs a warning (lines 113–116). ThatConnectorcomes up anyway, using the defaultconnectionTimeout. - A typo in an element name (2) is also a single warning. When no rule matches the path, the Digester only logs a warning (
Digester.javalines 1102–1104) and ignores the whole element. No<Conector port="8081">object was created and port 8081 never opened. - Only an XML syntax error (3) is a real failure. With
</Host>deleted, the parser stopped at line 152, where</Engine>is.parseServerXml()turns the exception into a warning log (lines 740–745) and returns without aServer; sincegetServer()isnull,configtestends with exit code 1. Withstart,Catalina.start()would logCannot start server, server instance is not configured(lines 912–915) andBootstrapwould callSystem.exit(1).
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.
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.
- Calling
start()on a NEW component runsinit()first (lines 160–161). This becomes important in section 4. - Calling
start()again on a component that is already running (STARTING_PREP,STARTING,STARTED) just logs at INFO and returns (lines 147–158). - Moving the state to
STARTINGis the subclass's job. If the state isn'tSTARTINGwhenstartInternal()returns, it counts as an invalid transition (lines 175–178). - There are two kinds of failure. If
startInternal()throws,handleSubClassException()sets the state toFAILEDand rethrows (lines 182–186, 406–417). If the subclass sets itself toFAILEDand 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
- A: a single
start()on a NEW component fired bothinitevents and all threestartevents. Thestartevent went out at the moment mystartInternal()calledsetState(STARTING). The secondstart()ended with one log line. - B: calling
init()twice throws aLifecycleException(lines 122–124). On the other hand,stop()on INITIALIZED does nothing (lines 218–220) — a component that never started has nothing to stop. - C: the exception from
startInternal()is rethrown to the caller and the state staysFAILED. Callingdestroy()onFAILEDfirst cleans up withstop()(lines 274–282). This skipsSTOPPING_PREP, so the state printed with thebefore_stopevent isFAILED(the comment at lines 232–236 explains why: so the component never looks available, even briefly). - D: setting itself to
FAILEDleads tostop()with no exception, ending inSTOPPED. This difference leads straight to section 5, "the server comes up even when a connector fails".
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.
- 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 onstartStopThreads. The default is 1 (line 244), and with 1 it usesInlineExecutorService, 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. setState(STARTING)is called after all the children have started. The server and the service fire their ownstartevent first and then call their children; a container fires itsstartevent only after its children are up.
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.
- Read the stack from the bottom up and the whole path from section 1 is there:
Bootstrap.main()→Bootstrap.start()→ (reflection,Method.invoke) →Catalina.start()→StandardServer→StandardService→StandardEngine→ContainerBase.startInternal()hands aStartChildtoInlineExecutorService→ the Host'sstart()sees NEW and callsinit()(line 161). The Host is initialized not in theinitphase but when its parent callsstart(). - The server and the service fire their
startevent (11, 13) before their children, while the engine and the host fire theirs (22, 24) after their children. That's the difference in wheresetState(STARTING)sits, mentioned at the end of section 3. - Web app deployment happened inside the Host's
startevent. Between 17 and 22 sit theDeploying web application directorylog line and the Context's events. WhenHostConfiggets thestartevent,deployApps()scanswebapps/(HostConfig.javalines 283–288, lines 1644–1667) and attaches the newContextwithhost.addChild()(line 1092). By then the Host is alreadySTARTING(available), so theaddChild()from section 2 callscontext.start()on the spot. Line 22 is printed after all that becauseHostConfigreceived the samestartevent before my listener did (HostConfigwas attached at the<Host>start tag, my listener at the<Listener>inside it). Deployment in detail is in part 10. - The connector is last (26–28). Only after the web apps are up does 8080 start taking requests.
- Everything is on the
mainthread. WithstartStopThreadsat 1, the children were started on the same thread too.
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
- (4) The second connector failed with a
BindExceptionwhile opening its port in theinitphase, but that was only logged and startup went on toServer startup. The one that answeredcurlis the first connector, which grabbed the port first (404 becausewebapps/is empty). Note also that the port clash shows up as a log line from theinitphase, notstart. By default a connector opens its port ininit(bindOnInitdefaults totrue,AbstractEndpoint.javaline 1317). - (5) With the property on, the connector's exception climbs to the service and the server, and
Catalina.load()turns it into anErrorand throws.Bootstrap.main()catches it, logsError running commandand exits with code 1.
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.
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).
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.
HELLOgets a single warning;SHUTDOWNgets an INFO line and stopping begins. Both are on[main]— the verymainthread that has been waiting since section 1.stopgoes from the top down, stopping Engine → Host → Context (36–45), and only then stops the connector (46–48). As seen in section 4, the connector was firstpause()d to block new requests, and its realstop()is called after the engine has stopped.- For
destroy, the service calls connector → executors → engine, in that order (StandardService.javalines 549–567). - Stopping with
kill -TERM, the same events come fromThread-1.CatalinaShutdownHookis aThreadwith no name, so the JVM named it, and the number may differ between environments.
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
- How
Bootstrapbuilds its class loaders (ClassLoaderFactory, path handling incatalina.properties):Bootstrap.javalines 159–243 — part 4 - Turning
server.xmlinto Java code ahead of time (-generateCode,-useGeneratedCode):Catalina.javalines 660–747 ${...}substitution insideserver.xml(Digester.updateBodyText(),PropertySource):java/org/apache/tomcat/util/digester/Digester.java<Listener>withoptional="true"(ListenerCreateRule):ListenerCreateRule.javalines 56–82- The listeners in the default
server.xml(VersionLoggerListener,AprLifecycleListener,JreMemoryLeakPreventionListener…):java/org/apache/catalina/startup/,java/org/apache/catalina/core/ - The
periodicevent every 10 seconds and containers' background processing (backgroundProcess()):StandardServer.javalines 881–902,ContainerBase.ContainerBackgroundProcessor LifecycleMBeanBase, which registers components with JMX:LifecycleMBeanBase.javalines 51–63 — part 16Lifecycle.SingleUse, for components used once and thrown away:Lifecycle.javalines 284–289
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).
- Part 1: How I Read It with AI, and the Map
- Part 2: The Big Picture — Server, Service, Connector and the Four Containers
- Part 5: The Connector and the NIO Endpoint — Which Threads Take a Connection
- Part 10: Deploying a Web App — From a WAR to a Running Context
- Part 16: Embedded Tomcat, JMX and Logging — and Wrapping Up the Series