源码分析
已经知道Tomcat启动以后,会启动6条线程,它们分别如下
Tomcat threads1
2
3
4
5
6"ajp-bio-8009-AsyncTimeout" daemon prio=5 tid=7f8738afe000 nid=0x115ad6000 waiting on condition [115ad5000]
"ajp-bio-8009-Acceptor-0" daemon prio=5 tid=7f8738b05800 nid=0x1159d3000 runnable [1159d2000]
"http-bio-8080-AsyncTimeout" daemon prio=5 tid=7f8735acb800 nid=0x1158d0000 waiting on condition [1158cf000]
"http-bio-8080-Acceptor-0" daemon prio=5 tid=7f8735acd000 nid=0x1157cd000 runnable [1157cc000]
"ContainerBackgroundProcessor[StandardEngine[Catalina]]" daemon prio=5 tid=7f8732850800 nid=0x111203000 waiting on condition [111202000]
"main" prio=5 tid=7f8735000800 nid=0x10843e000 runnable [10843c000]
其中5条是Dameon线程,而对于Java程序来说,当所有非Dameon程序都终止的时候,JVM就会退出,因此要想终止Tomcat就只需要将main()
这一条非Dameon线程终止即可。
Dameon线程又叫后台或者守护线程,它负责在程序运行期提供一种通用服务的线程,比如垃圾收集线程,非Dameon线程和Dameon线程的区别就在于当程序中所有的非Daemon线程都终止的时候,JVM会杀死余下的Dameon线程,然后退出。
接下来,一步步的分析如何来让main()
线程终止,要想终止它,还是得从Tomcat的启动中来寻找答案,在分析Tomcat容器启动的时候,在Catalina#start()
中有一段代码,接下来就来看看这段代码。
org.apache.catalina.startup.Catalina#start()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57/**
* Start a new server instance.
*/
public void start() {
if (getServer() == null) {
load();
}
if (getServer() == null) {
log.fatal("Cannot start server. Server instance is not configured.");
return;
}
long t1 = System.nanoTime();
// Start the new server
try {
getServer().start();
} catch (LifecycleException e) {
log.fatal(sm.getString("catalina.serverStartFail"), e);
try {
getServer().destroy();
} catch (LifecycleException e1) {
log.debug("destroy() failed for failed Server ", e1);
}
return;
}
long t2 = System.nanoTime();
if(log.isInfoEnabled()) {
log.info("Server startup in " + ((t2 - t1) / 1000000) + " ms");
}
// Register shutdown hook
// 1
if (useShutdownHook) {
if (shutdownHook == null) {
shutdownHook = new CatalinaShutdownHook();
}
Runtime.getRuntime().addShutdownHook(shutdownHook);
// If JULI is being used, disable JULI's shutdown hook since
// shutdown hooks run in parallel and log messages may be lost
// if JULI's hook completes before the CatalinaShutdownHook()
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
false);
}
}
// 2
if (await) {
await();
stop();
}
}
这里就是Tomcat关闭流程的入口代码。在代码中标注两处,首先来看标注1的地方。
- 标注1的代码:我们用到JVM的
Shutdown Hook
机制。一个简单的介绍,Shutdown Hook
是一个已经初始化但是还没有启动的线程,当JVM关闭的时候,它会启动并并发的运行所有已经注册过的Shutdown Hook
,知道这点,就来看看CatalinaShutdownHook线程做什么事情?它的代码如下。
org.apache.catalina.startup.Catalina.CatalinaShutdownHook#run()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* Shutdown hook which will perform a clean shutdown of Catalina if needed.
*/
protected class CatalinaShutdownHook extends Thread {
@Override
public void run() {
try {
if (getServer() != null) {
Catalina.this.stop();
}
} catch (Throwable ex) {
ExceptionUtils.handleThrowable(ex);
log.error(sm.getString("catalina.shutdownHookFail"), ex);
} finally {
// If JULI is used, shut JULI down *after* the server shuts down
// so log messages aren't lost
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).shutdown();
}
}
}
}
通过上面的代码,可以清楚的看到调用Catalina#stop()
。而Catalina#stop()
最终又是调用StandardServer#stop()
和destroy()
。通过这里,知道Tomcat利用shutdown hook
机制来在JVM关闭的时候关闭各个组件。但是JVM又是何时退出的呢?这就要来看标注为2的代码。
org.apache.catalina.startup.Catalina#stop()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41/**
* Stop an existing server instance.
*/
public void stop() {
try {
// Remove the ShutdownHook first so that server.stop()
// doesn't get invoked twice
if (useShutdownHook) {
Runtime.getRuntime().removeShutdownHook(shutdownHook);
// If JULI is being used, re-enable JULI's shutdown to ensure
// log messages are not lost
LogManager logManager = LogManager.getLogManager();
if (logManager instanceof ClassLoaderLogManager) {
((ClassLoaderLogManager) logManager).setUseShutdownHook(
true);
}
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
// This will fail on JDK 1.2. Ignoring, as Tomcat can run
// fine without the shutdown hook.
}
// Shut down the server
try {
Server s = getServer();
LifecycleState state = s.getState();
if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
&& LifecycleState.DESTROYED.compareTo(state) >= 0) {
// Nothing to do. stop() was already called
} else {
s.stop();
s.destroy();
}
} catch (LifecycleException e) {
log.error("Catalina.stop", e);
}
}
- 标注2的地方:首先判断await属性是否为true,如果为true就调用
await()
,调用完以后,再调用stop()
,接下来就来看await()
,而catalina#awit()
又调用StandardServer#awit()
,它的代码如下。
org.apache.catalina.startup.Catalina#await()1
2
3
4
5
6/**
* Await and shutdown.
*/
public void await() {
getServer().await();
}
org.apache.catalina.core.StandardServer#await()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126/**
* Wait until a proper shutdown command is received, then return.
* This keeps the main thread alive - the thread pool listening for http
* connections is daemon threads.
*/
@Override
public void await() {
// Negative values - don't wait on port - tomcat is embedded or we just don't like ports
if( port == -2 ) {
// undocumented yet - for embedding apps that are around, alive.
return;
}
if( port==-1 ) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
// Set up a server socket to wait on
try {
awaitSocket = new ServerSocket(port, 1,
InetAddress.getByName(address));
} catch (IOException e) {
log.error("StandardServer.await: create[" + address
+ ":" + port
+ "]: ", e);
return;
}
try {
awaitThread = Thread.currentThread();
// Loop waiting for a connection and a valid command
while (!stopAwait) {
ServerSocket serverSocket = awaitSocket;
if (serverSocket == null) {
break;
}
// Wait for the next connection
Socket socket = null;
StringBuilder command = new StringBuilder();
try {
InputStream stream;
try {
socket = serverSocket.accept();
socket.setSoTimeout(10 * 1000); // Ten seconds
stream = socket.getInputStream();
} catch (AccessControlException ace) {
log.warn("StandardServer.accept security exception: "
+ ace.getMessage(), ace);
continue;
} catch (IOException e) {
if (stopAwait) {
// Wait was aborted with socket.close()
break;
}
log.error("StandardServer.await: accept: ", e);
break;
}
// Read a set of characters from the socket
int expected = 1024; // Cut off to avoid DoS attack
while (expected < shutdown.length()) {
if (random == null)
random = new Random();
expected += (random.nextInt() % 1024);
}
while (expected > 0) {
int ch = -1;
try {
ch = stream.read();
} catch (IOException e) {
log.warn("StandardServer.await: read: ", e);
ch = -1;
}
if (ch < 32) // Control character or EOF terminates loop
break;
command.append((char) ch);
expected--;
}
} finally {
// Close the socket now that we are done with it
try {
if (socket != null) {
socket.close();
}
} catch (IOException e) {
// Ignore
}
}
// Match against our command string
boolean match = command.toString().equals(shutdown);
if (match) {
log.info(sm.getString("standardServer.shutdownViaPort"));
break;
} else
log.warn("StandardServer.await: Invalid command '"
+ command.toString() + "' received");
}
} finally {
ServerSocket serverSocket = awaitSocket;
awaitThread = null;
awaitSocket = null;
// Close the server socket and return
if (serverSocket != null) {
try {
serverSocket.close();
} catch (IOException e) {
// Ignore
}
}
}
}
通过上面的代码,可以看出在配置的端口上通过ServerSocket来监听一个请求的到来,如果请求的字符串和配置的字符串相同的话即跳出循环,这样的话就会运行stop()
,运行完以后,main线程就退出。
这里ServerSocket监听的端口,以及对比的字符串都是在conf/server.xml
中配置的,缺省情况下,配置如下:从这里可以看出监听端口为8005,关闭请求发送的字符串为SHUTDOWN。
看到这里,基本上已经清楚Tomcat的关闭就是通过在8005端口,发送一个SHUTDOWN字符串。那么就来实验一下。首先启动Tomcat,然后在终端运行如下指令。1
2
3
4
5
6telnet 127.0.0.1 8005
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
SHUTDOWN
Connection closed by foreign host.
运行telnet命令,并发送SHUTDOWN字符串以后,发现Tomcat就会退出await()
,然后执行stop()
最终停止。
但是一般情况下,停止tomcat都不会像上面那种方式来关闭,一般有两种方式来关闭:
ps aux | grep java ,kill -9
对于这种方式,比较简单粗暴会直接干掉进程。- 运行
shutdown.sh
这种方式其实最终也是向server发送一个SHUTDOWN字符串,接下来分析下第二种情况。
查看shutdown.sh
最终是调用catalina.sh
,并传递stop参数。查看catalina.sh
脚本,最终其实是调用org.apache.catalina.startup.Bootstrap#main
,并传递参数stop。查看Bootstrap#main()
,发现会调用org.apache.catalina.startup.Bootstrap#stopServer
,而Bootstrap#stopServer
通过反射调用org.apache.catalina.startup.Catalina#stopServer
,看看Catalina#stopServer()
,代码如下。
org.apache.catalina.startup.Catalina#stopServer()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86public void stopServer(String[] arguments) {
if (arguments != null) {
arguments(arguments);
}
Server s = getServer();
// 1
if( s == null ) {
// Create and execute our Digester
Digester digester = createStopDigester();
digester.setClassLoader(Thread.currentThread().getContextClassLoader());
File file = configFile();
FileInputStream fis = null;
try {
InputSource is =
new InputSource(file.toURI().toURL().toString());
fis = new FileInputStream(file);
is.setByteStream(fis);
digester.push(this);
digester.parse(is);
} catch (Exception e) {
log.error("Catalina.stop: ", e);
System.exit(1);
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// Ignore
}
}
}
} else {
// Server object already present. Must be running as a service
try {
s.stop();
} catch (LifecycleException e) {
log.error("Catalina.stop: ", e);
}
return;
}
// Stop the existing server
s = getServer();
// 2
if (s.getPort()>0) {
Socket socket = null;
OutputStream stream = null;
try {
socket = new Socket(s.getAddress(), s.getPort());
stream = socket.getOutputStream();
String shutdown = s.getShutdown();
for (int i = 0; i < shutdown.length(); i++) {
stream.write(shutdown.charAt(i));
}
stream.flush();
} catch (ConnectException ce) {
log.error(sm.getString("catalina.stopServer.connectException",
s.getAddress(),
String.valueOf(s.getPort())));
log.error("Catalina.stop: ", ce);
System.exit(1);
} catch (IOException e) {
log.error("Catalina.stop: ", e);
System.exit(1);
} finally {
if (stream != null) {
try {
stream.close();
} catch (IOException e) {
// Ignore
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
// Ignore
}
}
}
} else {
log.error(sm.getString("catalina.stopServer"));
System.exit(1);
}
}
分析一下标注的地方:
- 标注1的代码:此时因为是新开一个进程,并且
conf/server.xml
还没有解析,因此s是NULL,通过Digester解析conf/server.xml
,最终生成未初始化的StandardServer对象。 - 标注2的代码:向
standardServer#getPort
返回的端口(其实这里面返回即是conf/server.xml
中Server根节点配置的port和shutdown属性)发送standardServer#getShutdown()
返回的字符串,而默认情况下这个字符串就是SHUTDOWN。总结
Tomcat启动的时候的主线程会在8005端口(默认配置,可以更改)上建立socket监听,当关闭的时候,最终其实就是新起一个进程然后向Tomcat主线程监听的8005端口发送一个SHUTDOWN字符串,这样主线程就会结束,主线程结束以后,因为其它的线程都是dameon线程,不会退出。