本文是Tomcat源码阅读系列的第三篇文章,本系列前两篇文章如下:
Tomcat源码阅读系列(一)使用IntelliJ IDEA运行Tomcat6源码
Tomcat源码阅读系列(二)Tomcat总体架构
本文主要介绍Tomcat的启动和关闭的过程。
1. Tomcat启动过程
关于Tomcat的启动过程,本文主要介绍
Tomcat 6组件生命周期管理和
Tomcat启动过程两个方面,关于Tomcat组件生命周期的管理,Tomcat 6与Tomcat 7相差比较大,相对Tomcat 6的粗放的设计模式,Tomcat 7对组件声明周期的管理更加精细,是
模板设计模式的典型应用(使用模板方法模式所有支持生命周期管理的组件的生命周期各个阶段进行了总体管理,每个需要生命周期管理的组件只需要继承这个基类,然后覆盖对应的
钩子方法即可完成相应的生命周期阶段性的管理工作),而Tomcat 6仅仅是面向接口编程的规则的一种实现。关于Tomcat 7对组件生命周期的管理,可参看其源码,本文不再介绍,本文主要介绍Tomcat 6的启动过程。
1.1 Tomcat 6组件生命周期
从
Tomcat总体架构我们可知,Server、Service、Connector、Engine、Host、Context和Wapper的实现类均实现了
org.apache.catalina.Lifecycle接口。Tomcat组件的声明周期的管理也就是由这个接口定义的。其重要定义如下:
可以看出主要定义了start()和stop()两个声明周期阶段,相对而已Tomcat 7对声明周期的定义更加的精细和完整,Tomcat 7的LifeCycle中定义了init(初始化),start(启动),stop(停止),destory(销毁)四个生命周期。
Tomcat中的Server、Service、Connector、Engine、Host、Context和Wapper组件都需要启动和关闭,我们已经知道Lifecycle中声明的start的方法就是用来完成这项工作的。这些组件都派生了Lifecycle接口,分别实现了Lifecycle里的方法,也就拥有了管理自己生命周期(启动、销毁)的能力。
1.2 Tomcat启动过程
当用户输入执行startup命令时,Tomcat的主要流程如下:
当用户执行了./startup.sh 或者执行 ./catalina.bat start命令时,Tomcat 6主要执行了如上面时序图所示操作。
- load()方法主要用户加载server.xml等配置文件。
- init()和initialize()方法,主要用于初始化配置信息等,start主要是开启。
- 注意Connector的initialize()和start()方法,尤其是Connector中coyote组件与catalina组件的结合点CoyoteAdapter
- 注意StandardContext的start()方法。其中设置了很多重要的操作。如启动了ContainerBackgroundProcessor 线程,设置了应用级别的类加载器WebappClassLoader等,这些都是Tomcat架构当中要学习的地方。WebappClassLoader类加载器会新开博客进行说明。
接下来主要介绍一下让博主疑惑或者比较重要的需要注意的地方。
1.2.1 startup.bat与catalina.bat
我们在启动Tomcat的时候,可以调用startup.bat文件,也可以调用catalina.bat文件同时配合start参数启动Tomcat。而startup.bat文件的主要作用是启动Tomcat,而catalina.bat文件除了能够启动Tomcat之外,还可以停止Tomcat,查看Tomcat版本,设置Tomcat的启动模式等等,只要我们熟悉catalina.bat要输入的参数,我们就能玩转Tomcat,但是,绝大多数情况下,我们仅仅需要启动Tomcat让它作为容器即可,所以我们不需要更多复杂的操作,而startup.bat文件,就是做这个事情的工具,仅仅是让我们启动Tomcat。shutdown.bat文件与catalina.bat文件的关系与其类似。
startup.bat文件的作用就是找到catalina.bat文件,然后把start参数传递给它。
catalina.bat文件的作用是,设置一些环境变量,调用java命令传递给
org.apache.catalina.startup.Bootstrap类的main方法,从而启动Tomcat。
关于startup.bat文件和catalina.bat文件的详细说明,可以参考
查看tomcat启动文件都干点啥和
查看tomcat启动文件都干点啥---catalina.bat
1.2.2 Bootstrap与Catalina
通过startup.bat与catalina.bat文件,我们可以调用
org.apache.catalina.startup.Bootstrap类的main方法启动Tomcat,而Bootstrap主要是通过反射的方式调用
org.apache.catalina.startup.Catalina对应的方法,如在Bootstrap中的start()方法的实现如下:
/** * Start the Catalina daemon. */ public void start() throws Exception { if( catalinaDaemon==null ) init();//catalinaDaemon为在init()方法中创建的org.apache.catalina.startup.Catalina的实例 Method method = catalinaDaemon.getClass().getMethod("start", (Class [] )null); method.invoke(catalinaDaemon, (Object [])null);//反射调用Catalina的start方法 }
因此,Bootstrap与Catalina的关系与startup.bat和catalina.bat的关系类似,Bootstrap主要做一层代理,具体执行还在Catalina。
1.2.3 ContainerBackgroundProcessor 线程
StandardContext的start()方法中做了好多非常重要的操作,其中一点调用threadStart()方法就是开启了ContainerBackgroundProcessor线程,这个线程主要在Tomcat启动了后做一些操作,如比较重要的对Session过期的处理,每隔一秒钟扫描Manager中的Session集合并删除过期的Session。ContainerBackgroundProcessor做的主要工作在ContainerBase的backgroundProcess()方法如下:
/** * Execute a periodic task, such as reloading, etc. This method will be * invoked inside the classloading context of this container. Unexpected * throwables will be caught and logged. */ public void backgroundProcess() { if (!started) return; if (cluster != null) { try { cluster.backgroundProcess();//集群操作 } catch (Exception e) {//捕获异常,打一下日志,并不做处理! log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e); } } if (loader != null) { try { loader.backgroundProcess();//动态加载实现 } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e); } } if (manager != null) { try { manager.backgroundProcess();//删除过期session } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e); } } if (realm != null) { try { realm.backgroundProcess();//没有具体实现 } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e); } } Valve current = pipeline.getFirst(); while (current != null) { try { current.backgroundProcess();// } catch (Exception e) { log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e); } current = current.getNext(); } lifecycle.fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); }关于删除Session的操作,可以看一下具体实现,有助于对Session的理解。关于Session,本博会新开博文进行介绍。
1.2.4 Tomcat启动后的线程
我们知道Java程序启动以后,最终会以进程的形式存在,而Java进程中又会有很多条线程存在,因此最后我们就来看看Tomcat启动以后,到底启动了哪些线程,通过这些我们可以反过来验证我们对源代码的理解是否正确。接下来我们启动Tomcat,然后运行jstack -l <pid>来看看,在笔者的机器上面,jstack的输入如下所示:
"http-8080-1" daemon prio=6 tid=0x00000000080df800 nid=0x1968 in Object.wait() [0x00000000099ef000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x00000007d5f2f6b0> (a org.apache.tomcat.util.net.JIoEndpoint$Worker) at java.lang.Object.wait(Object.java:485) at org.apache.tomcat.util.net.JIoEndpoint$Worker.await(JIoEndpoint.java:458) - locked <0x00000007d5f2f6b0> (a org.apache.tomcat.util.net.JIoEndpoint$Worker) at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:484) at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: - None "http-8080-Acceptor-0" daemon prio=6 tid=0x0000000007e19000 nid=0x2f4 runnable [0x00000000091af000] java.lang.Thread.State: RUNNABLE at java.net.PlainSocketImpl.socketAccept(Native Method) at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:408) - locked <0x00000007d7bd7c38> (a java.net.SocksSocketImpl) at java.net.ServerSocket.implAccept(ServerSocket.java:462) at java.net.ServerSocket.accept(ServerSocket.java:430) at org.apache.tomcat.util.net.DefaultServerSocketFactory.acceptSocket(DefaultServerSocketFactory.java:61) at org.apache.tomcat.util.net.JIoEndpoint$Acceptor.run(JIoEndpoint.java:352) at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: - None "ContainerBackgroundProcessor[StandardEngine[Catalina]]" daemon prio=6 tid=0x000 00000075a1000 nid=0x2e8 waiting on condition [0x00000000090af000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1634) at java.lang.Thread.run(Thread.java:662) Locked ownable synchronizers: - None "main" prio=6 tid=0x000000000038b800 nid=0x175c runnable [0x000000000259e000] java.lang.Thread.State: RUNNABLE at java.net.PlainSocketImpl.socketAccept(Native Method) at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:408) - locked <0x00000007d60cbba0> (a java.net.SocksSocketImpl) at java.net.ServerSocket.implAccept(ServerSocket.java:462) at java.net.ServerSocket.accept(ServerSocket.java:430) at org.apache.catalina.core.StandardServer.await(StandardServer.java:430) at org.apache.catalina.startup.Catalina.await(Catalina.java:676) at org.apache.catalina.startup.Catalina.start(Catalina.java:628) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:289) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:414) Locked ownable synchronizers: - None
处理以上线程外还有TP-Monitor、TP-Processor两个线程,主要对线程进行监控和处理。
http-8080-1、http-8080-Acceptor-0和ContainerBackgroundProcessor[StandardEngine[Catalina]]线程都是daemon线程,而只有main不是daemon线程,这主要在后面的Tomcat的关闭过程有关。
- http-8080-1线程,主要用于处理请求,每个请求过来后,都新启一个线程处理。
- http-8080-Acceptor-0线程,主要在前面抗请求,每来一个请求,首次由http-8080-Acceptor-0接受这个请求,然后再新启上面的http-8080线程进行处理。
- ContainerBackgroundProcessor[StandardEngine[Catalina]]线程,主要执行一些定时任务,如定时删除过期Session,重载jsp等操作。
- main线程,唯一的非daemon线程,主要监听8005端口的SHUTDOWN命令(默认情况下),用于关闭Tomcat。
2. Tomcat关闭过程
如上面读Tomcat线程的描述一样,只有一个main线程是非daemon线程,其他都是daemon线程,对于Java程序来说,当所有非daemon程序都终止的时候,JVM就会退出,因此要想终止Tomcat就只需要将main这一条非daemon线程终止了即可。
daemon线程又叫后台或者守护线程,它负责在程序运行期提供一种通用服务的线程,比如垃圾收集线程,非daemon线程和daemon线程的区别就在于当程序中所有的非daemon线程都终止的时候,JVM会杀死余下的daemon线程,然后退出。
Tomcat主要有两种关闭的方式。
- Telnet连接到8005端口输入SHUTDOWN命令即可。
- 直接调用shutdown.bat文件。
2.1 Telnet连接特定端口输入特定命令
主要看一下上文中关于Tomcat的时序图,Catalina在执行start时,后面还会调用await()和stop()两个方法。其中catalina的awit方法调用了StandardServer#awit方法作为其实现。
/** * 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. */ public void await() { // Negative values - don't wait on port - tomcat is embedded or we just don't like ports // anything anything try { awaitSocket = new ServerSocket(port, 1, InetAddress.getByName("localhost")); } catch (IOException e) { log.error("StandardServer.await: create[" + 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 = null; long acceptStartTime = System.currentTimeMillis(); try { socket = serverSocket.accept(); socket.setSoTimeout(10 * 1000); // Ten seconds stream = socket.getInputStream(); } catch (SocketTimeoutException ste) { // This should never happen but bug 56684 suggests that // it does. log.warn(sm.getString("standardServer.accept.timeout", Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste); continue; } 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) { 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监听的端口,以及对比的字符串都是在conf/server.xml中配置的,缺省情况下,配置如下:,从这里可以看出监听端口为8005,关闭请求发送的字符串为SHUTDOWN。通过上面的代码,我们可以看出在配置的端口上通过ServerSocket来监听一个请求的到来,如果请求的字符串和配置的字符串相同的话即跳出循环,这样的话await方法就执行完成,然后运行stop方法,运行完了以后,main线程就退出了。
看到这里,我们基本上已经清楚了Tomcat的关闭就是通过在8005端口,发送一个SHUTDOWN字符串。那么我们就来实验一下。首先启动Tomcat,然后在终端运行如下指令:
telnet 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方法最终停止。
2.2 直接调用shutdown.bat文件
运行shutdown.bat这种方式其实
最终也是向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方法,代码如下:
public void stopServer(String[] arguments) { if (arguments != null) { arguments(arguments); } //.................anything // Stop the existing server s = getServer(); try { if (s.getPort()>0) { String hostAddress = InetAddress.getByName("localhost").getHostAddress(); Socket socket = new Socket(hostAddress, getServer().getPort()); OutputStream stream = socket.getOutputStream(); String shutdown = s.getShutdown(); for (int i = 0; i < shutdown.length(); i++) stream.write(shutdown.charAt(i)); stream.flush(); stream.close(); socket.close(); } else { log.error(sm.getString("catalina.stopServer")); System.exit(1); } } catch (IOException e) { log.error("Catalina.stop: ", e); System.exit(1); } }以上代码,向standardServer.getPort返回的端口(其实这里面返回即是conf/server.xml中Server根节点配置的port和shutdown属性)发送了s.getShutdown()返回的字符串,而默认情况下这个字符串就是SHUTDOWN。
2.3 硬杀
ps aux | grep java ,kill -9
3 其他
在看start()方法的时候,可能会看到这段代码Runtime.getRuntime().addShutdownHook(shutdownHook);这个方法的意思就是在jvm中增加一个关闭的钩子,当jvm关闭的时候,会执行系统中已经设置的所有通过方法addShutdownHook添加的钩子,当系统执行完这些钩子后,jvm才会关闭。所以这些钩子可以在jvm关闭的时候进行内存清理、对象销毁等操作。测试代码如下:
/** * Author: yangzhilong * Date: 2015/8/2 * Time: 0:30 */ public class TestShutdownHook { /** * @param args */ public static void main(String[] args) { // 定义线程1 Thread thread1 = new Thread() { public void run() { System.out.println("thread1..."); } }; // 定义线程2 Thread thread2 = new Thread() { public void run() { System.out.println("thread2..."); } }; // 定义关闭线程 Thread shutdownThread = new Thread() { public void run() { System.out.println("shutdownThread..."); } }; // jvm关闭的时候先执行该线程钩子 Runtime.getRuntime().addShutdownHook(shutdownThread); thread1.start(); thread2.start(); System.out.println("main..."); } }输出如下:
main... thread2... thread1... shutdownThread...shutdownThread 线程都是最后执行的(因为这个线程是在jvm执行关闭前才会执行)。