【tomcat】08 Server组件与Service组件

一、介绍

1、Server组件和Service组件是Tomcat核心组件中最外层级的两个组件,Server组件可以看成Tomcat的运行实例的抽象,而Service组件则可以看成Tomcat内的不同服务的抽象。

2、Server组件包含若干Listener组件、GlobalNamingResources组件及若干Service组件。

3、Service组件则包含若干Connector组件和Executor组件。

二、Server 组件

1、介绍

  1. Server组件是代表整个Tomcat的Servlet容器,从server.xml配置文件也可以看出它属于最外层组件。
  1. 默认配置了6个监听器组件,每个监听器负责各自的监听任务处理。
  1. GlobalNamingResources组件通过JNDI提供统一的命名对象访问接口,它的使用范围是整个Server。ServerSocket组件监听某个端口是否有SHUTDOWN命令,一旦接收到则关闭Server,即关闭Tomcat。

2、Server组件的作用

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

3、生命周期监听

  1. 初始化前、初始化中、初始化后、启动前、启动中、启动后、停止前、停止中、停止后、销毁中、销毁后等。为了在Server组件的某阶段执行某些逻辑,于是提供了监听器机制。
  1. 在Tomcat中实现一个生命周期监听器很简单,只要实现LifecycleListener接口即可,在lifecycleEvent方法中对感兴趣的生命周期事件进行处理。

1) AprLifecycleListener监听

a: Tomcat会使用APR本地库进行优化,通过JNI方式调用本地库能大幅提高对静态文件的处理能力。

b: AprLifecycleListener监听器对初始化前的事件和销毁后的事件感兴趣,在Tomcat初始化前,该监听器会尝试初始化APR库,假如能初始化成功,则会使用APR接受客户端的请求并处理请求。

c: 在Tomcat销毁后,该监听器会做APR的清理工作。

  1. JasperListener监听

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

  1. JreMemoryLeakPreventionListener监听

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

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

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

a: 在JRE库中某些类在运行时会以单例对象的形式存在,并且它们会存在很长一段时间,基本上是从Java程序启动到关闭。

b: JRE库的这些类使用上下文类加载器进行加载,并且保留了上下文类加载器的引用,所以将导致被引用的类加载器无法被回收,而Tomcat在重加载一个Web应用时正是通过实例化一个新的类加载器来实现的,旧的类加载器无法被垃圾回收器回收,导致内存泄漏。

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

3.2) JRE内存泄漏是因为线程启动另外一个线程并且新线程无止境地执行。

a) 在JRE库中存在某些类,当线程加载它时,它会创建一个新线程并且执行无限循环,新线程的上下文类加载器会继承父线程的上下文类加载器,所以新线程包含了上下文类加载器的应用,导致该类加载器无法被回收,最终导致内存泄漏。

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

3.2) JRE内存泄漏与线程的上下文类加载器有很大的关系。

a: 解决JRE内存泄漏,尝试让系统类加载器加载这些特殊的JRE库类。Tomcat中即使用了JreMemoryLeakPreventionListener监听器来做这些事
b: 代码

ClassLoader loader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
DriverManager.getDrivers();
try {
    Class.forName("sun.java2d.Disposer");
} catch (ClassNotFoundException cnfe) {
    Thread.currentThread().setContextClassLoader(loader);
}

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

3.4) JRE还有其他类也存在内存泄漏的可能,如javax.imageio.ImageIO、java.awt.Toolkit、sun.misc.GC、javax.security.auth.Policy、javax.security.auth.login.Configuration、java.security.Security、javax.xml.parsers.DocumentBuilderFactory、com.sun.jndi.ldap.LdapPoolManager等

3.5) 锁文件的情景主要由URLConnection默认的缓存机制导致,在Windows系统下当使用URLConnection的方式读取本地Jar包里面的资源时,它会将资源内存缓存起来,这就导致了该Jar包被锁。此时,如果进行重新部署将会失败,因为被锁的文件无法删除。为了解决锁文件问题,可以将URLConnection设置成默认不缓存,而这个工作也交由JreMemoryLeakPreventionListener完成。

在Tomcat启动时,实例化一个URLConnection,然后通过setDefaultUseCaches(false)设置成默认不缓存,这样后面使用URLConnection将不会因为缓存而锁文件。JreMemoryLeakPreventionListener监听器完成上面的工作即能避免JRE内存泄漏。

  1. GlobalResourcesLifecycleListener监听

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

  1. ThreadLocalLeakPreventionListener监听
  1. 主要解决ThreadLocal的使用可能带来的内存泄漏问题。监听器会在Tomcat启动后将监听Web应用重加载的监听器注册到每个Web应用上,当Web应用重加载时,该监听器会将所有工作线程销毁并再创建,以避免ThreadLocal引起内存泄漏。
  1. ThreadLocal引起的内存泄漏问题的根本原因也在于当垃圾回收器要回收时无法回收,因为使用了ThreadLocal的对象被一个运行很长时间的线程引用,导致该对象无法被回收。
  1. ThreadLocal导致内存泄漏的经典场景是Web应用重加载,当Tomcat启动后,对客户端的请求处理都由专门的工作线程池负责。线程池中线程的生命周期一般都会比较长,假如Web应用中使用了ThreadLocal保存AA对象,而且AA类由Webappclassloader加载,那么它就可以看成线程引用了AA对象。Web应用重加载是通过重新实例化一个Webappclassloader类加载器来实现的,由于线程一直未销毁,旧的Webappclassloader也无法被回收,导致了内存泄漏。
  1. 解决ThreadLocal内存泄漏最彻底的方法就是当Web应用重加载时,把线程池内的所有线程销毁并重新创建,这样就不会发生线程引用某些对象的问题了。Tomcat中处理ThreadLocal内存泄漏的工作其实主要就是销毁线程池原来的线程,然后创建新线程。分两步,第一步先将任务队列堵住,不让新任务进来;第二步将线程池中所有线程停止。
  1. ThreadLocalLeakPreventionListener监听器的工作就是实现当Web应用重加载时销毁线程池的线程并重新创建新线程,以此避免ThreadLocal内存泄漏。
  1. NamingContextListener监听

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

4、全局命名资源

  1. Server组件包含了一个全局命名资源,它提供的命名对象通过ResourceLink可以给所有Web应用使用。
  1. ContextResources、ContextEjb、 ContextEnvironment、ContextLocalEjb、MessageDestinationRef
    ContextResourceEnvRef、ContextResourceEnvRef、 ContextResourceLink、ContextService
  1. Tomcat启动时将server.xml配置文件里面的GlobalNamingResources节点通过Digester框架映射到一个NamingResources对象。这个对象里面包含了不同类型的资源对象,同时会创建一个NamingContextListener监听器,这个监听器负责在Tomcat初始化启动期间完成对命名资源的所有创建、组织、绑定等工作,使之符合JNDI标准。而创建、组织、绑定等是根据NamingResources对象描述的资源属性进行处理的,绑定的路径由配置文件的Resource节点的name属性决定,name即为JNDI对象树的分支节点,例如,name为“jdbc/myDB”,那么此对象就可通过“java:jdbc/myDB”访问,而树的位置应该是jdbc/myDB,但在Web应用中是无法直接访问全局命名资源的。因为要访问全局命名资源,所以资源都必须放在Server组件中。

5、监听SHUTDOWN命令

  1. Server会另外开放一个端口用于监听关闭命令,这个端口默认为8005,此端口与接收客户端请求的端口并非同一个。客户端传输的第一行如果能匹配关闭命令(默认为SHUTDOWN),则整个Server将会关闭
  2. Tomcat中有两类线程,一类是主线程,另外一类是daemon线程。当Tomcat启动时,Server将被主线程执行,其实就是完成所有的启动工作,包括启动接收客户端和处理客户端报文的线程,这些线程都是daemon线程。所有启动工作完成后,主线程将进入等待SHUTDOWN命令的环节,它将不断尝试读取客户端发送过来的消息,一旦匹配SHUTDOWN命令则跳出循环。主线程继续往下执行Tomcat的关闭工作。最后主线程结束,整个Tomcat停止。
  1. 监听SHUTDOWN命令简单了解:打开本地8005端口监听客户端,一旦有客户端连接,就尝试读取客户端的命令,如果客户端发送的命令为SHUTDOWN,则跳出循环,让整个主线程执行完毕,也就意味着程序执行完关闭。假如输入的命令并非为SHUTDOWN,则进去下一个循环,等待下一个客户端的连接。

三、Service 组件

1、介绍

  1. Service组件是若干Connector组件和Executor组件组合而成的概念。Connector组件负责监听某端口的客户端请求,不同的端口对应不同的Connector。Executor组件在Service抽象层面提供了线程池,让Service下的组件可以共用线程池。默认情况下,不同的Connector组件会自己创建线程池来使用,而通过Service组件下的Executor组件则可以实现线程池共享,每个Connector组件都使用Service组件下的线程池。除了Connector组件之外,其他的组件也可以使用。

2、Tomcat中线程池的实现

  1. “池”的引入是为了在某些场景下提高系统某些关键节点的性能和效率,最典型的例子就是数据库连接池。数据库连接的建立和销毁都是很耗时耗资源的操作。为了查询数据库中某条记录,最原始的一个过程是建立连接,发送查询语句,返回查询结果,销毁连接。假如仅仅是一个很简单的查询语句,那么建立连接与销毁连接两个步骤就已经占所有时间消耗的绝大部分,效率显然让人无法接受。于是想到尽可能减少创建和销毁连接操作,连接相对于查询是无状态的,不必每次查询都重新生成和销毁连接,可以维护这些通道维护以供下一次查询或其他操作使用。维护这些管道的工作就交给了“池”。
  1. 线程池也是类似于数据库连接池的一种池。线程是为多任务而引入的概念,每个线程在任意时刻执行一个任务,假如多个任务要并发执行,则要用到多线程技术。每个线程都有自己的生命周期,以创建为始,以销毁为末。线程的运行阶段占整个生命周期的比重不同。引入了线程池,它的核心思想就是把运行阶段尽量拉长,对于每个任务的到来,不是重复建立、销毁线程,而是重复利用之前建立的线程执行任务。

3、自己实现一个线程池

  1. 思路在系统启动时建立一定数量的线程并做好线程维护工作,一旦有任务到来即从线程池中取出一条空闲的线程执行任务。线程池的属性包含初始化线程数量、线程数组、任务队列。

a: 初始化线程数量指线程池初始化的线程数,线程数组保存了线程池中的所有线程,任务队列指添加到线程池中等待处理的所有任务。

b: 线程池里有多个个线程,池里线程的工作就是不断循环检测任务队列中是否有需要执行的任务,如果有,则处理并移出任务队列。可以说线程池中的所有线程的任务就是不断检测任务队列并不断执行队列中的任务。

  1. 使用线程池时只须实例化一个对象,构造函数就会创建相应数量的线程并启动线程,启动的线程无限循环地检测任务队列,执行方法execute()仅仅把任务添加到任务队列中。需要注意的一点是,所有任务都必须实现Runnable接口,这是线程池的任务队列与工作线程的约定,JUC工具包作者DougLea当时如此规定,工作线程检测任务队列并调用队列的run()方法,假如重新写一个线程池,就完全可以自己定义一个不一样的任务接口。一个完善的线程池并不像下面的例子那样简单,它需要提供启动、销毁、增加工作线程的策略,最大工作线程数,各种状态的获取等操作,而且工作线程也不可能始终做无用循环,需要对任务队列使用wait、notify优化,或者将任务队列改用为阻塞队列。
package com.example.tomcat.http;


import java.util.LinkedList;
import java.util.List;

/**
 * @author haoxiansheng
 */
public final class ThreadPool {
    private final int worker_num;

    private WorkerThread[] workerThreads;

    private List<Runnable> taskQueue = new LinkedList<>();

    private static ThreadPool threadPool;

    public ThreadPool(int worker_num) {
        this.worker_num = worker_num;
        workerThreads = new WorkerThread[worker_num];
        for (int i = 0; i < worker_num; i++) {
            workerThreads[i] = new WorkerThread();
            workerThreads[i].start();
        }
    }

    public void execute(Runnable task) {
        synchronized (taskQueue) {
            taskQueue.add(task);
        }
    }

    private class WorkerThread extends Thread {
        @Override
        public void run() {
            Runnable runnable = null;
            while (true) {
                synchronized (taskQueue) {
                    if (!taskQueue.isEmpty()) {
                        runnable = taskQueue.remove(0);
                        runnable.run();
                    }
                }
            }
        }
    }
}

  1. 自己实现线程池处理很容易产生死锁问题,同时线程池内的状态同步操作不当也可能导致意想不到的问题。除此之外,还有很多其他的并发问题,除非是很有并发的经验才能尽可能减少可能的错误。
  1. 直接使用JDK的JUC工具包即可,它是由DougLea编写的优秀并发程序工具,仅线程池就已经提供了好多种类的线程池,实际开发中可以根据需求选择合适的线程池。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值