06.Server组件

147 阅读8分钟

Server

1.介绍

Server是最顶级的组件,它代表Tomcat的运行实例,在一个JVM中只包含一个Server,从server.xml 配置文件也可以看出它属于最外层组件,它包含一个或多个Service

2.作用

  1. 监听8005端口发过来的shutdown命令,用于关闭整个容器
  2. 包含多组服务(Service)负责管理和启动各个Service
  3. 为了方便扩展,它引入了监听器方式,所以它也包含了Listener组件
  4. 为了方便在Tomcat中集成JNDI,引入了GlobalNamingResources组件

3.结构

介绍

  1. 默认配置了6个监听器,每个监听器负责各自的监听任务处理
  2. GlobalNamingResources组件通过JNDI提供统一的命名对象访问接口,它的使用范围是整个Server
  3. ServerSocket组件监听某个端口是否有SHUTDOWN 命令,一旦接收到了则关闭Server

图示

8dfb1dba8a6c3d59c202f39ecc6c10c6.png

4.六大监听器

AprLifecycleListener监听器

有时候,Tomcat会使用APR本地库进行优化,通过JNI方式调用本地库能大幅提高对静态文件的处理能力。AprLifecycleListener监听器对初始化前的事件和销毁后的事件感兴趣,在Tomcat初始化前,该监听器会尝试初始化APR 库,假如能初始化成功,则会使用APR接受客户端的请求并处理请求。在Tomcat销毁后,该监听器会做APR的清理工作

JasperListener监听器

在Tomcat初始化前该监听器会初始化Jasper组件,Jasper是Tomcat 的JSP编译器核心引擎,用于在Web应用启动前初始化Jasper

JreMemoryLeakPreventionListener监听器

介绍

该监听器主要解决JRE内存泄露和锁文件的一种措施,该监听器会在Tomcat初始化时使用系统类加载器先加载一些类和设置缓存属性,以避免内存泄漏和锁文件

JRE内存泄漏
内存泄漏的原因

内存泄漏的根本原因在于当垃圾回收器要回收时无法回收本该被回收的对象。假如一个待回收对象被另外一个生命周期很长的对象引用,那么这个对象将无法被回收

介绍

JRE内存泄漏是因为上下文类加载器导致的内存泄漏

两种情况

在JRE库中某些类在运行时会以单例对象的形式存在,并且它们会存在很长一段时间,基本上是从Java程序启动到关闭。JRE库的这些类使用上下文类加载器进行加载,并且保留了上下文类加载器的引用,所 以将导致被引用的类加载器无法被回收,而Tomcat在重加载一个Web应用时正是通过实例化一个新的类加载器来实现的,旧的类加载器无法被垃圾回收器回收,导致内存泄漏

另外一种JRE内存泄漏是因为线程启动另外一个线程并且新线程无止境地执行。在JRE库中存在某些类,当线程加载它时,它会创建一个新线程并且执行无限循环,新线程的上下文类加载器会继承父线程的上下文类加载器,所以新线程包含了上下文类加载器的引用,导致该类加载器无法被回收,最终导致内存泄漏

例子1

如图所示,某上下文类加载器为WebappClassloader的线程加载JRE的DriverManager类,此过程将导致WebappClassloader被引用,后面该WebappClassloader将无法被回收,发生内存泄漏

ca9980a5362d28257da3a0035ced1e59.png

例子2

如图所示,某上下文类加载器为Webappclassloader的线程加载JRE的Disposer类,此时该线程会创建一个新的线程,新线程的上下文类加载器为 Webappclassloader,随后新线程将进入一个无限循环的执行中,最终该 Webappclassloader将无法被回收,发生内存泄漏

3125204c892afcb356ec6bca0abf6f61.png

解决方案

可以看到JRE内存泄漏与线程的上下文类加载器有很大的关系。为了解决JRE内存泄漏,尝试让系统类加载器加载这些特殊的JRE库类。Tomcat中即使用了JreMemoryLeakPreventionListener监听器来做这些事

代码与含义
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
DriverManager.getDrivers();
try {
    Class.forName("sun.java2d.Disposer");
} catch(Exception e) {
    
}
Thread.currentThread().setContextClassLoader(loader);

在Tomcat启动时,先将当前线程的上下文类加载器设置为系统类加载器,再执行DriverManager.getDrivers()和Class.forName("sun.java2d.Disposer"),即会加载这些类,此时的线程上下文为系统类加载器,加载完这些特殊的类后再将上下文类加载器还原。此时,如果Web应用使用到这些类,由于它们已经加载到系统类加载器中,因此重启Web应用时不会存在内存泄漏

除了上面两个类之外,JRE还有其他类也存在内存泄漏的可能

锁文件

介绍

锁文件的情景主要由URLConnection默认的缓存机制导致,在Windows系统下当使用URLConnection的方式读取本地Jar包里面的资源时,它会将资源内存缓存起来,这就导致了该Jar包被锁。此时,如果进行重新部署将会失败,因为被锁的文件无法删除

解决方案

为了解决锁文件问题,可以将URLConnection设置成默认不缓存,而这个工作也交由JreMemoryLeakPreventionListener完成

代码与含义
URL url = new URL("jar:file://dummy.jar!/");
URLConnection uConn = url.openConnection();
uConn.setDefaultUseCaches(false);

在Tomcat启动时,实例化一个URLConnection,然后通过setDefaultUseCaches(false) 设置成默认不缓存,这样后面使用URLConnection将不会因为缓存而锁文件

GlobalResourcesLifecycleListener监听器

该监听器主要负责实例化Server组件里面JNDI资源的MBean,并提交由JMX管理。此监听器对生命周期内的启动事件和停止事件感兴趣,它会在启动时为JNDI创建 MBean,而在停止时销毁MBean

ThreadLocalLeakPreventionListener监听器

ThreadLocal与内存泄漏

ThreadLocal引起的内存泄漏问题的根本原因也在于当垃圾回收器要回收时无法回收,因为使用了ThreadLocal的对象被一个运行很长时间的线程引用,导致该对象无法被回收

介绍

该监听器主要解决ThreadLocal的使用可能带来的内存泄漏问题。该监听器会在Tomcat启动后将监听Web应用重加载的监听器注册到每个Web应用上,当Web应用重加载时,该监听器会将所有工作线程销毁并再创建,以避免ThreadLocal引起内存泄漏

例子

ThreadLocal导致内存泄漏的经典场景是Web应用重加载,如图所示。当Tomcat启动后,对客户端的请求处理都由专门的工作线程池负责。线程池中线程的生命周期一般都会比较长,假如Web应用中使用了ThreadLocal 保存AA对象,而且AA类由Webappclassloader加载那么它就可以看成线程引用了AA对象。Web应用重加载是通过重新实例化一个 Webappclassloader类加载器来实现的,由于线程一直未销毁,旧的Webappclassloader也无法被回收,导致了内存泄漏

822069e589fc212bd24ebd558a71ef5d.png

解决方案

解决ThreadLocal内存泄漏最彻底的方法就是当Web应用重加载时,把线程池内的所有线程销毁并重新创建,这样就不会发生线程引用某些对象的问题了。如图所示,Tomcat中处理ThreadLocal内存泄漏的工作其实主要就是销毁线程池原来的线程,然后创建新线程。这分两步做,第一步先将任务队列堵住,不让新任务进来;第二步将线程池中所有线程停止

ac3acd1edeb0ceb7a9b971d3edd2dcef.png

ThreadLocalLeakPreventionListener监听器的工作就是实现当Web应用重加载时销毁线程池的线程并重新创建新线程,以此避免ThreadLocal内存泄漏

NamingContextListener监听器

该监听器主要负责Server组件内全局命名资源在不同生命周期的不同操作,在Tomcat启动时创建命名资源、绑定命名资源,在Tomcat停止前解绑命名资源、反注册MBean

5.两大组件

全局命名资源

与 JNDI 有关省略...

监听shutdown命令

介绍

Server会另外开放一个端口用于监听关闭命令,这个端口默认为8005,此端口与接收客户端请求的端口并非同一个。客户端传输的第一行如果能匹配关闭命令(默认为SHUTDOWN),则整个Server将会关闭

原理

如图所示,Tomcat中有两类线程,一类是主线程,另外一类是daemon线程。当Tomcat启动时,Server将被主线程执行,其实就是完成所有的启动工作,包括启动接收客户端和处理客户端报文的线程,这些线程都是daemon线程。所有启动工作完成后,主线程将进入等待SHUTDOWN命令的环节,它将不断尝试读取客户端发送过来的消息,一旦匹配SHUTDOWN命令则跳出循环。主线程继续往下执行Tomcat的关闭工作。最后主线程结束,整个Tomcat停止

8028549a2a135f594f61761f4d222b33.png

代码

a2cde92f5994cb88dd478538acbeaee8.png