05、Tomcat 内核详解 - Server组件与Service组件

1.Server组件

作为Tomcat最外层的核心组件,Server组件的作用主要有以下几个。

  • 提供了监听器机制,用于在Tomcat整个生命周期中对不同事件进行处理;
  • 提供了Tomcat容器全局的命名资源实现;
  • 监听某个端口以接收SHUTDOWN命令;

1.生命周期监听器

为了在Server组件的某阶段执行某些逻辑,于是提供了监听器机制。在Tomcat中实现一个生命周期监听器很简单,只要实现LifecycleListener接口即可,在lifecycleEvent方法中对感兴趣的生命周期事件进行处理。

1、 AprLifecycleListener监听器:;

通过JNI的方式调用本地库能够大幅的提高对静态文件的处理能力。

该监听器会初始化APR库,假如能初始化成功,则会使用APR接受客户端的请求并处理请求。在Tomcat销毁后,该监听器会做APR的清理工作;

1、 JasperListener监听器:;

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

1、 JreMemoryLeakPreventionListener监听器:;

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

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

类加载器无法被回收将会导致内存泄露;

JRE内存泄露与线程上下文类加载器有很大的关系。为了解决JRE内存泄露,尝试让系统类加载器加载这些特殊的JRE库类。

在Tomcat启动的时候,先将当前线程的上下文类加载器设置为系统类加载器,再执行加载一些特殊类的动作,此时的线程上下文为系统类加载器,加载完这些特殊的类之后再将上下文类加载器还原。

此时如果Web应用使用到这些类,由于它们已经加载到系统类加载器中,因此重启Web应用的时候不会存在内存泄露。

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

DriverManager、javax.imageio.ImageIo、java.awt.Toolkit、sun.misc.GC、java.security.auth.Policy、java.security.auth.login.Configuration、java.security.Security、javax.xml.parsers.DocumentBuilderFactory、com.sun.jndi.ldapPoolManager

【锁文件问题】

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

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

1、 GlobalResourcesLifecycleListener监听器:;

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

1、 ThreadLocalLeakPreventionListener监听器:;

该监听器主要解决ThreadLocal的使用可能带来的内存泄露问题。当web应用重新加载的时候,该监听器会将所有工作线程销毁并创建,以避免ThreadLocal引起内存泄露;

ThreadLocal引起的内存泄露根本原因也是因为当垃圾回手器要回收的时候,无法进行回收,因为使用了ThreadLocal的对象被一个运行很长时间的线程引用,导致该对象无法被回收;

解决ThreadLocal内存泄露的问题最彻底的方法就是当Web应用重新加载的时候,把线程池内的所有线程销毁并重新创建,这样就不会发生线程引用某些对象的问题了。

Tomcat中处理ThreadLocal内存泄露的工作其实主要就是销毁线程池原来的线程,然后创建新的线程。这分为两个步骤:

第一步:先将任务队列堵住,不让新的任务进来;

第二步:将线程池中所有的线程停止;

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

1、 NamingContextListener监听器:;

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

2.全局命名资源

在Tomcat启动的时候,通过Digester框架将server.xml的描述映射到对象,在Server组件中创建NamingResource和NamingContextListener两个对象。监听器将在启动初始化的时候利用ContextResource里面的属性创建命名上下文,并组织成树状;

在Web应用中是无法访问全局命名资源的。因为要访问全局命名资源,所以这些资源都必须放置在Server组件中;

3.监听SHUTDWON命令

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

实现这个功能:

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

2.Service组件

Service组件是一个简单地组件,Service组件是若干Connector组件和Executor组件组合而成的概念。

Connector组件负责监听某个端口的客户端请求,不同的端口对应不同的Connector。

Executor组件在Service抽象层面提供了线程池,让Service下的组件可以通用线程池;

使用池的技术:就是为了尽量的减少创建和销毁的连接操作;

线程池技术:核心思想就是把运行阶段尽量的拉长,对于每个任务的到来,不是重复建立、销毁线程,而是重复利用之前的线程执行任务;

其中一种解决方案就是在系统建立一定的数量的线程并做好线程维护工作,一旦有任务到来的时候,即从线程池中取出一条空闲的线程执行任务。原理听起来比较简单,但是现实中的对于一条线程,一旦调用start 方法后,就将运行任务直到任务完成,随后JVM 将对线程对象进行GC 回收;

所以需要换一种思维来解决问题,让这些线程启动之后通过一个无限循环来执行指定的任务。

一个线程池的属性包含初始化线程数量、线程数组、任务队列。初始化线程数量指线程池初始化的线程数,线程数组保存了线程池中所有的线程,任务队列值添加到线程池中等待处理的所有任务。

于是:线程池中的所有线程的任务就是不断检测任务队列并不断执行队列中的任务;

【一个完善的线程池】

需要提供启动、销毁、增加工作线程的策略,最大工作线程数、各种状态的获取等操作,而且工作线程也不可能始终做无用循环,需要对任务队列使用wait 、notify 优化,或者将任务队列改用为阻塞队列;

【JDK 的JUC 工具包】优秀的并发程序工具包,仅仅线程池就已经提供了好多种类的线程池,实际开发中可以根据需求选择合适的线程池;

了解了线程池的原理,其实我们并不提倡重复造轮子的行为。

如果自己实现线程池,很容易产生死锁的问题,同时线程内的同步状态操作不当也可能会导致意想不到的问题;