Tomcat
Tomcat核心: Http服务器(应用服务器,即网络框架)+Servlet容器。
HTTP服务器是指网络框架,负责接收客户端的连接。
Servlet容器主要是指接口,进行业务逻辑处理。
Tomcat 设计了两个核心组件连接器(Connector)和容器(Container),分别用于对HTTP服务器和Servlet容器的实现。
Tomcat核心组件
A.Service组件
一个Server代表一个Tomcat实例(而不是一个应用),可以包含多个Service组件,每个Service组件包含了1个Engine容器和多个Connector组件,其中多个Connector组件用于处理不同协议的请求。通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。一个Service可以通过在Engine容器下配置多个Host(一个host表示一个应用)来实现配置多个应用,但是一般每个Service只配置一个Host,即通常配置一个应用。
B.连接器Connector组件
1.连接器对 Servlet 容器屏蔽了不同的应用层协议及 I/O 模型,无论是 HTTP 还是AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。
2.连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异,ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint 负责底层 Socket通信,Proccesor 负责应用层协议解析。连接器通过适配器 Adapter 调用容器。
3.ProtocolHandler 组件
a.连接器用 ProtocolHandler 来处理网络连接和应用层协议,包含了 2 个重要部件:
EndPoint 和 Processor。EndPoint处理TCP/IP协议,Processor处理HTTP/AJP协议。
b.EndPoint(包含了2个子组件Acceptor和SocketProcessor)
EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。其中 Acceptor 用于监听 Socket 连接请求。
SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行,而这个线程池叫作执行器(Executor)。
4.Adapter组件
负责将TomcatRequest转成ServletRequest,然后调用容器service方法,同时传入ServletRequest作为service参数
C.容器Container组件
(Engine、Host、Context 和 Wrapper都是容器,Wrapper为最底层的容器,封装了Servlet,表示一个Servlet。每种容器都实现了Container接口)
1.Tomcat 设计了4 种容器,分别是 Engine、Host、Context 和 Wrapper。这 4 种容器不是平行关系,而是父子关系。
a.即1个Service包含1个Engine,1个Engine包含多个Host,一个Host包含多个Context,一个Context包含多个Wrapper。一个Wrapper表示一个Servlet。
b.一个Context表示一个应用
2.Tomcat 采用组合模式来管理这些容器(类似ViewGroup和View)
具体实现方法是,所有容器组件都实现了Container 接口,因此组合模式可以使得用户对单容器对象和组合容器对象的使用具有一致性。
Tomcat工作原理
(Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的)
A.请求定位 Servlet 的过程(Tomcat 是用 Mapper 组件来完成这个任务的。)
Tomcat 是用 Mapper 组件来完成这个任务的。Mapper 组件的功能就是将用户请求的 URL 定位到一个 Servlet,它的工作原理是:Mapper 组件里保存了 Web 应用的配置信息,其实就是容器组件与访问路径的映射关系,比如 Host 容器里配置的域名、Context 容器里的 Web 应用路径,以及 Wrapper 容器里 Servlet 映射的路径,你可以想象这些配置信息就是一个多层次的 Map。当一个请求到来时,Mapper 组件通过解析请求 URL 里的域名和路径,再到自己保存的 Map 里去查找,就能定位到一个 Servlet。一个请求 URL 最后只会定位到一个 Wrapper 容器,也就是一个 Servlet。
B.请求在容器中的调用过程
连接器中的 Adapter 会调用容器的 Service 方法来执行 Servlet,最先拿到请求的是Engine 容器,Engine 容器对请求做一些处理后,会把请求传给自己子容器 Host 继续处理,依次类推,最后这个请求会传给 Wrapper 容器,Wrapper 会调用最终的 Servlet 来处理。那么这个调用过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。
Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。
Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。
整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用 getNext.invoke()
来触发下一个 Valve 调用。每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到。Basic Valve 处于 Valve链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。
调用顺序依次是从Engine->Host->Context->Wrapper->Servlet。
每一个容器都有一个 Pipeline 对象
整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve。
Wrapper 容器的最后一个 Valve 会创建一个 Filter 链,并调用 doFilter() 方法,最终会调
到 Servlet 的 service 方法。
Valve 和 Filter 的区别:
1.Valve 是 Tomcat 的私有机制,与 Tomcat 的基础架构 /API 是紧耦合的。
Servlet API 是公有的标准,所有的 Web 容器包括 Jetty 都支持 Filter 机制。
2.Valve 工作在 Web 容器级别,拦截所有应用的请求;而 Servlet Filter 工作在
应用级别,只能拦截某个 Web 应用的所有请求。
Tomcat生命周期实现
通过一种通用的、统一的方法来管理组件的生命周期。包括LifeCycle接口、LifeCycle事件。
tomcat启动流程
可参考
tomcat启动流程
主要是按照init():初始化组件、start():启动组件、stop():停止组件、destroy():销毁组件 流程来执行。
Tomcat的 I/O 模型
Tomcat 支持的多种 I/O 模型和应用层协议。Tomcat 支持的 I/O 模型有:BIO、NIO、AIO(异步非阻塞式IO/Nio2Endpoint/NIO.2)、APR
I/O 调优实际上是连接器类型的选择(默认用NIO,WINDOWS系统可用NIO.2)
a.一般情况下默认都是 NIO,在绝大多数情况下都是够用的,除非你的 Web 应用用到了 TLS 加密传输,而且对性能要求极高,这个时候可以考虑 APR,因为 APR 通过 OpenSSL 来处理 TLS 握手和加密 / 解密。OpenSSL 本身用C 语言实现,它还对 TLS 通信做了优化,所以性能比 Java 要高。
b.Windows系统可选NIO.2。
如果你的 Tomcat 跑在Windows 平台上,并且 HTTP 请求的数据量比较大,可以考虑 NIO.2,这是因为Windows 从操作系统层面实现了真正意义上的异步 I/O,如果传输的数据量比较大,异步I/O 的效果就能显现出来。
NIO 和 NIO.2 最大的区别是,一个是同步一个是异步。异步最大的特点是,应用程序不需要自己去触发数据从内核空间到用户空间的拷贝。
Tomcat是如何实现非阻塞I/O的?
(而 NioEndpoint 利用 Java NIO API 实现了多路复用 I/O 模型,基于主从Reactor多线程模型设计)
在 Tomcat 中,EndPoint 组件的主要工作就是处理 I/O,而 NioEndpoint 利用 Java NIO API 实现了多路复用 I/O 模型。Tomcat的NioEndpoint 是基于主从Reactor多线程模型设计的(看netty 深入Linux内核理解epoll)。
Tomcat设计精髓
A.Tomcat扩展了线程池
思考:Tomcat是如何扩展java线程池的?
(自定义了拒绝策略、重写了TaskQueue offer方法,让线程池可以创建新的线程)
Tomcat线程池默认实现了StandardThreadExecutor。Tomcat 线程池和 Java 原生线程池的区别:
1.自定义了拒绝策略,Tomcat 在线程总数达到最大数时,队列也满了的时候,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。
java 原生线程池:在标准 Java 原生线程池(如 ThreadPoolExecutor)中,当线程池的线程数达到最大线程数且任务队列已满时,会立即根据配置的拒绝策略(如 AbortPolicy、CallerRunsPolicy 等)处理任务。
2.默认的java原生线程池中,只有当线程数达到核心线程数(>=)且任务队列满的时候才会开启新的线程去处理,如果任务队列的Integer.MAX_VALUE,则队列会一直放不满,则线程数到达核心线程数的时候就会一直无法开启新的线程。Tomcat对这个进行了处理,其TaskQueue 重写了 LinkedBlockingQueue 的 offer 方法。如果线程不够用了且线程数小于最大线程数,会去创建新的线程。目的:在任务队列的长度无限制的情况下,让线程池有机会创建新的线程。
Java 原生线程池:在默认的 ThreadPoolExecutor 实现中,只有当线程数达到核心线程数且任务队列已满时,才会创建新的线程。如果任务队列长度非常大(如 Integer.MAX_VALUE),那么在队列未满时线程池不会创建新的线程,线程数始终被限制在核心线程数之内。这可能导致任务处理速度较慢,因为任务会被放入队列中,而不是立即创建新线程处理。
B.Tomcat 使用了对象池技术。
Tomcat调优
(需要根据压测确定,而不是事先就可计算好的)
(tomcat调优主要是线程池的并发调优)
线程池的并发调优(给 Tomcat 的线程池设置合适的参数)
这里面最核心的就是如何确定 maxThreads 的值这个值是需要压测在确定的,而不是用公式来计算的。
sever.xml中配置线程池(默认没有配置,使用的都是默认参数)
1 <!‐‐
2 namePrefix: 线程前缀
3 maxThreads: 最大线程数,默认设置 200,一般建议在 500 ~ 800,根据硬件设施和业务
来判断
4 minSpareThreads: 核心线程数,默认设置 25
5 prestartminSpareThreads: 在 Tomcat 初始化的时候就初始化核心线程
6 maxQueueSize: 最大的等待队列数,超过则拒绝请求 ,默认 Integer.MAX_VALUE
7 maxIdleTime: 线程空闲时间,超过该时间,线程会被销毁,单位毫秒
8 className: 线程实现类,默认org.apache.catalina.core.StandardThreadExecutor
9
10 <Executor name=“tomcatThreadPool” namePrefix=“catalina‐exec‐Fox”
11 prestartminSpareThreads=“true”
12 maxThreads=“500” minSpareThreads=“8” maxIdleTime=“10000”/>
13
14 <Connector port=“8080” protocol=“HTTP/1.1” executor=“tomcatThreadPool”
15 connectionTimeout="20000"16 redirectPort=“8443” URIEncoding=“UTF‐8”/>
Tomcat类加载机制
A.初步了解JVM类加载机制
1.双亲委派机制(先从父加载器找,父加载器再委托其父加载器,即一直向上委托,然后由上往下执行)
加载某个类时会先委托父加载器寻找目标类,父加载器再委托给其上层父加载器加载,一直向上委托,直到达到顶层的启动类加载器(Bootstrap ClassLoader),然后由顶层加载器开始加载类,如果顶层加载器加载不到则,则有下层依次加载类。这就是双亲委派机制。
2.为什么要设计双亲委派机制?(防止核心api被篡改、避免类重复加载)
a.沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改
b.避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性。
3.JVM类加载器
Java中有 3 个类加载器,另外你也可以自定义类加载器
a.BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如rt.jar。
b.ExtClassLoader 是扩展类加载器,用来加载\jre\lib\ext目录下 JAR 包。扩展加载器的 #getParent() 方法返回 null ,但实际上扩展类加载器的父类加载器就是启动类加载器。
c.AppClassLoader 应用类加载器,又称为系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。程序可以通过 #getSystemClassLoader() 来获取系统类加载器。
d.自定义类加载器,用来加载自定义路径下的类。
B.Tomcat类加载机制
(JVM类默认加载机制无法实现Tomcat各场景,比如应用隔离,所以Tomcat自定义了自己的类加载机制,打破了双亲委派机制)
1.思考: Tomcat是如何隔离Web应用的?
(通过自定义类加载器WebAppClassLoader,每个应用都创建了自己的WebAppClassLoader实例,打破了双亲委派机制来实现隔离)
a.Tomcat 自定义了一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。在WebAppClassLoader中对findClass和loadClass两个方法进行了重写。
b.WebAppClassLoader具体实现
(如何打破双亲委派机制?
1.为每个应用创建了自己的实例,不同类加载器实例加载的类是互相隔离的 2.WebAppClassLoader 自定义了find Class和loadClass方法,优先从Web 应用本地目录即/WebApp/WEB-INF/下查找类看是否加载)
Tomcat 自定义了自己的类加载器 WebAppClassLoader,每个应用都创建了自己的WebAppClassLoader实例, WebAppClassLoader打破了双亲委托机制,对findClass和loadClass两个方法进行了重写。在类加载的时候,它首先自己尝试去查找某个类(调用findClass方法),如果找不到才再代理给父类加载器,其目的是更快的加载到 Web 应用中自己定义的类,这样可以提升了加载性能。找到类之后,开始加载类(调用loadClass方法),先从本地缓存加载该类,如果本地缓存没找到,则委托给WebAppClassLoader的父类系统类加载器AppClassLoader,如果AppClassLoader没加载过,就委托给ExtClassLoader去加载(ExtClassLoader加载的时候其又会自动委托给 BootstrapClassLoader 去加载),如果这些系统的ClassLoader都加载失败的话才交给Tomcat自定义的WebAppClassLoader去加载类,这样可以防止自己的类覆盖了JRE的核心类。
c.所以Tomcat委派模型和JVM双亲委派的区别是?
1).Tomcat为每个应用创建了WebAppClassLoader的实例,不同类加载器实例加载的类是互相隔离的
相互隔离的意思是:同一个JVM内,两个相同包名和类名的类对象是可以共存的。
只要他们的类加载器不一样即可,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类加载器也是同一个才能认为他们是同一个。
2).JVM双亲委派在加载类的时候,是先委托给父类,父类再委托给父类,直到委托到BootstrapClassLoader。即最终是先由BootstrapClassLoader进行查找加载,然后再到ExtClassLoader,等这些classloader找不到了在由AppClassLoader进行查找和加载。
而Tomcat委派模型是先从Web 应用本地目录开始进行查找相应的类名(类名是路径+名称),如果没找到类名,则从父类加载器去查找直到查找到BootstrapClassLoader。如果没找到就抛出ClassNotFound错误。如果找到类名,就交由ExtClassLoader进行加载相应的类名,如果加载成功则返回,加载失败则由其子加载器加载,一直由WebAppClassLoader进行加载。
2.Tomcat自定义了多个类加载器,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader
a.它们分别加载/common/、/server/、/shared/和/WebApp/WEB-INF/中的Java类库。其中WebApp类加载器和Jsp类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个Jsp类加载器。
b.tomcat1.7开始对把CommonClassLoader、CatalinaClassLoader、SharedClassLoader这3个classloader进行了合并
3.线程上下文加载器(主要是用于解决跨ClassLoader加载类)
a.默认情况下,如果一个类由类加载器 A 加载,那么这个类的依赖类也是由相同的类加载器加载(默认情况),这也叫全盘负责委托机制。
b.思考:如果spring作为共享第三方jar包,由SharedClassLoader加载,但是业务类在web目录下,不在SharedClassLoader的加载路径下,那spring如何加载web应用目录下的业务bean呢?
1)Tomcat 为每个 Web 应用创建一个 WebAppClassLoarder 类加载器,并在启动Web 应用的线程里设置将其为线程上下文加载器,这样 Spring 在启动时就将线程上下文加载器取出来,用来加载 Bean。
Thread.currentThread().getContextClassLoader()
2)线程上下文加载器其他应用场景(JDBC驱动加载和Tomcat热加载和热部署等)
线程上下文加载器不仅仅可以用在 Tomcat 和 Spring 类加载的场景里,核心框架类需要加载具体实现类时都可以用到它,比如我们熟悉的 JDBC 就是通过上下文类加载器来加载不同的数据库驱动的。
线程上下文加载器在Tomcat热加载和热部署会用到。
4.tomcat类加载器关系图