Tomcat 处理请求大致流程和架构

请求流程

截屏2021-11-12 下午4.41.36
  • 1.HTTP服务器会把请求信息使⽤ServletRequest对象封装起来
  • 2.进⼀步去调⽤Servlet容器中某个具体的Servlet
  • 3.在 2中,Servlet容器拿到请求后,根据URL和Servlet的映射关系,找到相应的Servlet
  • 4.如果Servlet还没有被加载,就⽤反射机制创建这个Servlet,并调⽤Servlet的init⽅法来完成初始化
  • 5.接着调⽤这个具体Servlet的service⽅法来处理请求,请求处理结果使⽤ServletResponse对象封装
  • 6.把ServletResponse对象返回给HTTP服务器,HTTP服务器会把响应发送给客户端

Tomact整体架构

tomcat有两个主要功能:

  • 和客户端浏览器进⾏交互,进⾏socket通信,将字节流和Request/Response等对象进⾏转换
  • 处理业务逻辑

其中负责客户端浏览器进⾏交互的叫连接器(connector)

负责处理业务逻辑的叫容器(container)

Coyote

简介

Coyote 是Tomcat 中连接器的组件名称 , 是对外的接⼝。客户端通过Coyote与服务器建⽴连接、发送请求并接受响应 。 其主要功能如下:

  • Coyote 封装了底层的⽹络通信(Socket 请求及响应处理)
  • Coyote 使Catalina 容器(容器组件)与具体的请求协议及IO操作⽅式完全解耦
  • Coyote 将Socket 输⼊转换封装为 Request 对象,进⼀步封装后交由Catalina 容器进⾏处理,处理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写⼊输出流
  • Coyote 负责的是具体协议(应⽤层)和IO(传输层)相关内容

Tomcat Coyote ⽀持的协议与 IO模型如下:

协议:

HTTP/1.1默认协议,大部分web采用的协议
AJP用于和WX集成(比如Apache),实现了对静态资源的优化和集群部署,当前支持 AJP/1.3
HTTP/2HTTP 2.0大大提高了web的性能,从8.5和9.0后版本后开始支持

IO:

NIO默认IO,非阻塞IO,采用Java NIO类库实现
NIO2异步IO,采用jdk7的NIO2类库实现
APR采用Apche可移植库实现,是C/C++编写的本地库,使用时需要单独安装APR库

在 8.0 之前 ,Tomcat 默认采⽤的I/O⽅式为 BIO,之后改为 NIO。 ⽆论 NIO、NIO2 还是 APR, 在性能⽅⾯均优于以往的BIO。 如果采⽤APR, 甚⾄可以达到 Apache HTTP Server 的影响性能。

内部组件及流程

截屏2021-11-12 下午5.29.30

组件作⽤描述
EndPointEndPoint 是 Coyote 通信端点,即通信监听的接⼝,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint⽤来实现TCP/IP协议的
ProcessorProcessor 是Coyote 协议处理接⼝,如果说EndPoint是⽤来实现TCP/IP协议的,那么Processor⽤来实现HTTP协议,Processor接收来⾃EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应⽤层协议的抽象
ProtocolHandlerCoyote 协议接⼝, 通过Endpoint 和 Processor , 实现针对具体协议的处理能⼒。Tomcat 按照协议和I/O 提供了6个实现类 : AjpNioProtocol ,AjpAprProtocol, AjpNio2Protocol , Http11NioProtocol ,Http11Nio2Protocol ,Http11AprProtocol
Adapter由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat定义了⾃⼰的Request类来封装这些请求信息。ProtocolHandler接⼝负责解析请求并⽣成Tomcat Request类。但是这个Request对象不是标准的ServletRequest,不能⽤Tomcat Request作为参数来调⽤容器。Tomcat设计者的解决⽅案是引⼊CoyoteAdapter,这是适配器模式的经典运⽤,连接器调⽤CoyoteAdapter的Sevice⽅法,传⼊的是Tomcat Request对象,CoyoteAdapter负责将Tomcat Request转成ServletRequest,再调⽤容器

Catalina

简介

Tomcat是⼀个由⼀系列可配置(conf/server.xml)的组件构成的Web容器,⽽Catalina是Tomcat的servlet容器。从另⼀个⻆度来说,Tomcat 本质上就是⼀款 Servlet 容器, 因为 Catalina 才是 Tomcat 的核⼼ , 其他模块都是为Catalina 提供⽀撑的。 ⽐如 : 通过 Coyote 模块提供链接通信,Jasper 模块提供 JSP 引擎,Naming 提供JNDI 服务,Juli 提供⽇志服务。

截屏2021-11-12 下午5.44.28

可以认为Tomcat就是⼀个Catalina的实例,因为Catalina是Tomcat的核⼼,Tomcat 启动的时候会初始化Catalina实例,Catalina实例通过加载server.xml完成其他实例的创建,创建并管理⼀个Server,Server创建并管理多个服务,每个服务⼜可以有多个Connector和⼀个Container。

tomact各个部分的分工如下:

  • Catalina:负责解析Tomcat的配置⽂件(server.xml) , 以此来创建服务器Server组件并进⾏管理

  • Server:表示整个Catalina Servlet容器以及其它组件,负责组装并启动Servlet引擎,Tomcat连接器。Server通过实现Lifecycle接⼝,提供了⼀种优雅的启动和关闭整个系统的⽅式

  • Service:服务是Server内部的组件,⼀个Server包含多个Service。它将若⼲个Connector组件绑定到⼀个Container

  • Container:容器,负责处理⽤户的servlet请求,并返回对象给⽤户的模块

截屏2021-11-12 下午5.54.27
Container结构

Container组件下有⼏种具体的组件,分别是Engine、Host、Context和Wrapper。这4种组件(容器)是⽗⼦关系。Tomcat通过⼀种分层的架构,使得Servlet容器具有很好的灵活性。

  • Engine:表示整个Catalina的Servlet引擎,⽤来管理多个虚拟站点,⼀个Service最多只能有⼀个Engine,但是⼀个引擎可包含多个Host

  • Host:代表⼀个虚拟主机,或者说⼀个站点,可以给Tomcat配置多个虚拟主机地址,⽽⼀个虚拟主机下可包含多个Context

  • Context:表示⼀个Web应⽤程序, ⼀个Web应⽤可包含多个Wrapper

  • Wrapper:表示⼀个Servlet,Wrapper 作为容器中的最底层,不能包含⼦容器

上述组件的配置其实就体现在conf/server.xml中。

Tomcat配置

tomcat的配置文件是conf/server.xml

主要标签结构

<!--
    Server 根元素,创建⼀个Server实例,⼦标签有 ListenerGlobalNamingResourcesService
-->
<Server>
<!--定义监听器-->
	<Listener/>
	<!--定义服务器的全局JNDI资源 -->
	<GlobalNamingResources/>
	<!--
    	定义⼀个Service服务,⼀个Server标签可以有多个Service服务实例
	-->
	<Service/>
</Server>

Server

<!--

port:关闭服务器的监听端⼝

shutdown:关闭服务器的指令字符串

-->

<Server port="8005" shutdown="SHUTDOWN">

<!-- 以⽇志形式输出服务器 、操作系统、JVM的版本信息 -->

<Listener className="org.apache.catalina.startup.VersionLoggerListener" />

<!-- Security listener. Documentation at /docs/config/listeners.html

<Listener className="org.apache.catalina.security.SecurityListener" />

-->

<!--APR library loader. Documentation at /docs/apr.html -->

<!-- 加载(服务器启动) 和 销毁 (服务器停⽌) APR。 如果找不到APR库, 则会输出⽇志, 并不影响 Tomcat启动 -->

<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />

<!-- Prevent memory leaks due to use of particular java/javax APIs-->

<!-- 避免JRE内存泄漏问题 -->

<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />

<!-- 加载(服务器启动) 和 销毁(服务器停⽌) 全局命名服务 -->

<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />

<!-- 在Context停⽌时重建 Executor 池中的线程, 以避免ThreadLocal 相关的内存泄漏 -->

<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />

<!-- Global JNDI resources 

Documentation at /docs/jndi-resources-howto.html

GlobalNamingResources 中定义了全局命名服务-->



<GlobalNamingResources>

​	<!-- Editable user database that can also be used by

​	UserDatabaseRealm to authenticate users

​	-->

​	<Resource name="UserDatabase" auth="Container" 

​					type="org.apache.catalina.UserDatabase"

​					description="User database that can be updated and saved"

​					factory="org.apache.catalina.users.MemoryUserDatabaseFactory"

​						pathname="conf/tomcat-users.xml" />

</GlobalNamingResources>

<!-- A "Service" is a collection of one or more "Connectors" that share

a single "Container" Note: A "Service" is not itself a "Container",

so you may not define subcomponents such as "Valves" at this level.

Documentation at /docs/config/service.html

-->

<Service name="Catalina">

 ...

</Service>

</Server>

Service

<!--

该标签⽤于创建 Service 实例,默认使⽤ org.apache.catalina.core.StandardService。默认情况下,Tomcat 仅指定了Service 的名称, 值为 "Catalina"。Service ⼦标签为 : Listener、Executor、Connector、Engine,

其中:

Listener ⽤于为Service添加⽣命周期监听器,

Executor ⽤于配置Service 共享线程池,

Connector ⽤于配置Service 包含的链接器,

Engine ⽤于配置Service中链接器对应的Servlet 容器引擎

-->

<Service name="Catalina">

...

</Service>

Executor

<!--默认情况下,Service 并未添加共享线程池配置。 如果我们想添加⼀个线程池, 可以在<Service> 下添加如下配置:

name:线程池名称,⽤于 Connector中指定

namePrefix:所创建的每个线程的名称前缀,⼀个单独的线程名称为namePrefix+threadNumber

maxThreads:池中最⼤线程数

minSpareThreads:活跃线程数,也就是核⼼池线程数,这些线程不会被销毁,会⼀直存在

maxIdleTime:线程空闲时间,超过该时间后,空闲线程会被销毁,默认值为6000(1分钟),单位毫秒

maxQueueSize:在被执⾏前最⼤线程排队数⽬,默认为Int的最⼤值,也就是⼴义的⽆限。除⾮特殊情况,这个值 不需要更改,否则会有请求不会被处理的情况发⽣

prestartminSpareThreads:启动线程池时是否启动 minSpareThreads部分线程。默认值为false,即不启动

threadPriority:线程池中线程优先级,默认值为5,值从1到10

className:线程池实现类,未指定情况下,默认实现类为

org.apache.catalina.core.StandardThreadExecutor。如果想使⽤⾃定义线程池⾸先需要实现org.apache.catalina.Executor接⼝

-->

<Executor name="commonThreadPool"

namePrefix="thread-exec-"

maxThreads="200"

minSpareThreads="100"

maxIdleTime="60000"

maxQueueSize="Integer.MAX_VALUE"

prestartminSpareThreads="false"

threadPriority="5"

className="org.apache.catalina.core.StandardThreadExecutor"/>

Connector

Connector 标签⽤于创建链接器实例

默认情况下,server.xml 配置了两个链接器,⼀个⽀持HTTP协议,⼀个⽀持AJP协议

⼤多数情况下,我们并不需要新增链接器配置,只是根据需要对已有链接器进⾏优化

<!--port:端⼝号,Connector ⽤于创建服务端Socket 并进⾏监听, 以等待客户端请求链接。如果该属性设置为0, Tomcat将会随机选择⼀个可⽤的端⼝号给当前Connector 使⽤

protocol:当前Connector ⽀持的访问协议。 默认为 HTTP/1.1 , 并采⽤⾃动切换机制选择⼀个基于 JAVA的NIO 的链接器或者基于本地APR的链接器(根据本地是否含有Tomcat的本地库判定)

connectionTimeOut:Connector 接收链接后的等待超时时间, 单位为 毫秒。 -1 表示不超时。

redirectPort:当前Connector 不⽀持SSL请求, 接收到了⼀个请求, 并且也符合security-constraint 约束,需要SSL传输,Catalina⾃动将请求重定向到指定的端⼝。

executor:指定共享线程池的名称, 也可以通过maxThreads、minSpareThreads 等属性配置内部线程池。

URIEncoding:⽤于指定编码URI的字符编码, Tomcat8.x版本默认的编码为 UTF-8 , Tomcat7.x版本默认为ISO-8859-1

<!--org.apache.coyote.http11.Http11NioProtocol , ⾮阻塞式 Java NIO 链接器-->

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"

redirectPort="8443" />

<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

可以使⽤共享线程池:

<Connector port="8080"

protocol="HTTP/1.1"

executor="commonThreadPool"

maxThreads="1000"

minSpareThreads="100"

acceptCount="1000"

maxConnections="1000"

connectionTimeout="20000"

compression="on"

compressionMinSize="2048"

disableUploadTimeout="true"

redirectPort="8443"

URIEncoding="UTF-8" />

Engine

Engine 表示 Servlet 引擎

<!--

name: ⽤于指定Engine 的名称, 默认为Catalina
defaultHost:默认使⽤的虚拟主机名称, 当客户端请求指向的主机⽆效时, 将交由默认的虚拟主机处
理, 默认为localhost
-->

<Engine name="Catalina" defaultHost="localhost">

...

</Engine>

Host

Host 标签⽤于配置⼀个虚拟主机

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">

 ...

</Host>

Context

Context 标签⽤于配置⼀个Web应⽤

<Host name="www.abc.com" appBase="webapps" unpackWARs="true" autoDeploy="true">
        <!--docBase:Web应⽤⽬录或者War包的部署路径。可以是绝对路径,也可以是相对于 Host appBase的相对路径。
        path:Web应⽤的Context 路径。如果我们Host名为localhost, 则该web应⽤访问的根路径为:http://localhost:8080/web_demo。-->
        <Context docBase="/Users/test/web_demo" path="/web_demo">
        
        </Context> 
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" 
          prefix="localhost_access_log" suffix=".txt" 
          pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>

Tomcat 启动流程

截屏2021-11-12 下午7.32.31

Tomcat 处理请求流程

当一个请求到达tomcat时,tomcat的mapper组件会把url和host、context、wrapper进行映射,比如http://localhost:8080/web_demo/resume/addResume,

host是负责映射localhost、context负责web_demo、wrapper负责resume/addResume 这样就可以找到对应的Servlet出来业务

请求处理流程如下:

截屏2021-11-12 下午7.40.01

Mapper组件体系结构:

截屏2021-11-12 下午7.40.27

JVM类加载机制和Tomcat类加载机制

JVM类加载机制

JVM 的类加载加载流程是由类加载器完成的。
java中主要有以下几种类加载器:

  • 引导类加载器 BootstrapClassLoader: 也叫启动类加载器,负责加载JRE的lib目录下的核心类库,例如rt.jar等

  • 扩展类加载器 ExtClassLoader:负责加载扩展库JAVA_HOME/lib/ext目录下的jar中的类,如classpath中的jre,javax.*或者java.ext.dir指定位置中的类

  • 系统类加载器 AppClassLoader:负责加载ClassPath路径下的类包,主要是加载自己定义的那些类

  • 自定义加载器 :负责加载自定义路径下的类

当 JVM 运⾏过程中,⽤户⾃定义了类加载器去加载某些类时,会按照下⾯的步骤(⽗类委托机制)

1.⽤户⾃⼰的类加载器,把加载请求传给⽗加载器,⽗加载器再传给其⽗加载器,⼀直到加载器树的顶层

2.最顶层的类加载器⾸先针对其特定的位置加载,如果加载不到就转交给⼦类

3.如果⼀直到底层的类加载都没有加载到,那么就会抛出异常 ClassNotFoundException

因此,按照这个过程可以想到,如果同样在 classpath 指定的⽬录中和⾃⼰⼯作⽬录中存放相同的class,会优先加载 classpath ⽬录中的⽂件

Jvm类加载器的父子(委托)关系
截屏2021-11-12 下午9.33.54
什么是双亲委派机制

当某个类被加载时,当前的类加载器会先把这个任务委托给父类加载器,递归这个操作,只有父类加载器找不到这类的时候,才会自己去加载这个类。
例如加载Test对象 AppClassLoader会先把这个任务交给ExtClassLoader,ExtClassLoader又会上交给BootstrapClassLoader,因为BootstrapClassLoader负责加载核心类库的类,ExtClassLoader负责加载扩展类,因此在它们负责的路径下是找不到这个类的,最后返回给AppClassLoader完成加载。

双亲委派机制的作用
  • 防止类被重复加载:向上委托的时候会判断该类是否已经被加载过了,如果父类加载器已经加载了,就不用再重复加载了.

  • 防止核心Api被随意篡改:通过委托⽅式,不会去篡改核⼼.class,即使篡改也不会去加载,即使加载也不会是同⼀个.class对象了。不同的加载器加载同⼀个.class也不是同⼀个.class对象。这样保证了class执⾏安全(如果⼦类加载器先加载,那么我们可以写⼀些与java.lang包中基础类同名的类, 然后再定义⼀个⼦类加载器,这样整个应⽤使⽤的基础类就都变成我们⾃⼰定义的类了。)

Jvm类加载器源码

类加载器的loadClass方法:java.lang.ClassLoader#loadClass(java.lang.String, boolean)

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
  • getClassLoadingLock;对目标类进行加锁,防止多个线程同时加载同一个类,造成重复加载。
  • findLoadedClass(name); 判断目标类是否已经被加载,还未被加载则返回null。
  • 双亲委派关键逻辑:
if (parent != null) {
       c = parent.loadClass(name, false);
} else {
      c = findBootstrapClassOrNull(name);
}

可以看到在这里获取当前类加载器的父类加载器,如果不等于null则由父类加载器加载目标类,并且父类加载器也同样实现了该逻辑。
如果为null则调用findBootstrapClassOrNull方法。
那么什么时候或者什么样的类加载器,父类加载器才会为空呢?

扩展类加载器的上级加载器为null,这是因为启动类加载器不是由java编写的,所以在jvm中为null.
回到上面的源码,也就是当目标类委托到扩展类加载器之后继续向上委托时就会执行findBootstrapClassOrNull方法。

  • c = findClass(name);当目标类c为null是,则会调用findClass查找该类再自己的加载路径范围之类。如果没有找到,则返回给下级类加载器。

简单总结就是先找父亲加载,不行就给儿子加载。

Tomcat的类加载机制

Tomcat 的类加载机制相对于 Jvm 的类加载机制做了⼀些改变。没有严格的遵从双亲委派机制,也可以说打破了双亲委派机制,这主要是因为tomcat是一个web服务器需要支持部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。

Tomcat类加载器

img

  • 引导类加载器 BootstrapClassLoader和扩展类加载器 ExtClassLoader的作⽤不变

  • 系统类加载器 AppClassLoader正常情况下加载的是 CLASSPATH 下的类,但是 Tomcat 的启动脚本并未使⽤该变量,⽽是加载tomcat启动的类,⽐如bootstrap.jar,通常在catalina.bat或者catalina.sh中指定。位于CATALINA_HOME/bin下

  • commonClassLoader:通用类加载器加载Tomcat使用以及应用通用的一些类,位于CATALINA_HOME/lib下, 比如servlet-api.jar

  • catalinaClassLoader:tomcat容器私有的类加载器,加载服务器内部可⻅类对于Webapp不可见

  • sharedClassLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见

  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;每个 webappClassLoader加载自己的目录下的class文件(本应⽤程序 /WEB-INF/classes 和 /WEB-INF/lib 下的类),不会传递给父类加载器,打破了双 亲委派机制。

tomcat 8.5 默认改变了严格的双亲委派机制

  • ⾸先从 Bootstrap Classloader加载指定的类(用来加载基础类, 比如:Object,String等)
  • 如果未加载到,则从 /WEB-INF/classes加载
  • 如果未加载到,则从 /WEB-INF/lib/*.jar 加载
  • 如果未加载到,则依次从 System、Common、Shared 加载(在这最后⼀步,遵从双亲委派机制)

Tomcat类加载器源码

WebappClassLoaderBase的loadClass方法:org.apache.catalina.loader.WebappClassLoaderBase#loadClass(java.lang.String, boolean)

    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {
            if (log.isDebugEnabled()) {
                log.debug("loadClass(" + name + ", " + resolve + ")");
            }
            Class<?> clazz = null;

            // Log access to stopped class loader
            checkStateForClassLoading(name);

            // (0) Check our previously loaded local class cache
            /**
             * 检查该类是否已经被webapp类加载器加载。
             */
            clazz = findLoadedClass0(name);
            if (clazz != null) {
                if (log.isDebugEnabled()) {
                    log.debug("  Returning class from cache");
                }
                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            }

            // (0.1) Check our previously loaded class cache
            /**
             * 该方法直接调用findLoadedClasso本地方法,findLoadedClass0方法会检查jvm缓存中是否加载过此类(jvm内存)
             */
            clazz = findLoadedClass(name);
            if (clazz != null) {
                if (log.isDebugEnabled()) {
                    log.debug("  Returning class from cache");
                }
                if (resolve) {
                    resolveClass(clazz);
                }
                return clazz;
            }

            // (0.2) Try loading the class with the system class loader, to prevent
            //       the webapp from overriding Java SE classes. This implements
            //       SRV.10.7.2
            String resourceName = binaryNameToPath(name, false);

            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                // Use getResource as it won't trigger an expensive
                // ClassNotFoundException if the resource is not available from
                // the Java SE class loader. However (see
                // https://bz.apache.org/bugzilla/show_bug.cgi?id=58125 for
                // details) when running under a security manager in rare cases
                // this call may trigger a ClassCircularityError.
                // See https://bz.apache.org/bugzilla/show_bug.cgi?id=61424 for
                // details of how this may trigger a StackOverflowError
                // Given these reported errors, catch Throwable to ensure any
                // other edge cases are also caught
                URL url;
                if (securityManager != null) {
                    PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
                    url = AccessController.doPrivileged(dp);
                } else {
                    url = javaseLoader.getResource(resourceName);
                }
                tryLoadingFromJavaseLoader = (url != null);
            } catch (Throwable t) {
                // Swallow all exceptions apart from those that must be re-thrown
                ExceptionUtils.handleThrowable(t);
                // The getResource() trick won't work for this class. We have to
                // try loading it directly and accept that we might get a
                // ClassNotFoundException.
                tryLoadingFromJavaseLoader = true;
            }

            /**
             * 尝试通过系统类加载器(AppClassLoader)加载类,防止webapp重写jdk的类。
             * 例如:webapp加载一个java.lang.String类,是不被允许的。
             */
            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (0.5) Permission to access this class when using a SecurityManager
            if (securityManager != null) {
                int i = name.lastIndexOf('.');
                if (i >= 0) {
                    try {
                        securityManager.checkPackageAccess(name.substring(0,i));
                    } catch (SecurityException se) {
                        String error = sm.getString("webappClassLoader.restrictedPackage", name);
                        log.info(error, se);
                        throw new ClassNotFoundException(error, se);
                    }
                }
            }

            boolean delegateLoad = delegate || filter(name, true);

            // (1) Delegate to our parent if requested
            /**
             * 是否委派给父类加载.
             */
            if (delegateLoad) {
                if (log.isDebugEnabled()) {
                    log.debug("  Delegating to parent classloader1 " + parent);
                }
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled()) {
                            log.debug("  Loading class from parent");
                        }
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

            // (2) Search local repositories
            /**
             * 使用webapp加载
             */
            if (log.isDebugEnabled()) {
                log.debug("  Searching local repositories");
            }
            try {
                clazz = findClass(name);
                if (clazz != null) {
                    if (log.isDebugEnabled()) {
                        log.debug("  Loading class from local repository");
                    }
                    if (resolve) {
                        resolveClass(clazz);
                    }
                    return clazz;
                }
            } catch (ClassNotFoundException e) {
                // Ignore
            }

            // (3) Delegate to parent unconditionally
            /**
             * 如果webapp类加载器没有找到,则交给父加载器
             */
            if (!delegateLoad) {
                if (log.isDebugEnabled()) {
                    log.debug("  Delegating to parent classloader at end: " + parent);
                }
                try {
                    clazz = Class.forName(name, false, parent);
                    if (clazz != null) {
                        if (log.isDebugEnabled()) {
                            log.debug("  Loading class from parent");
                        }
                        if (resolve) {
                            resolveClass(clazz);
                        }
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }
        }

        throw new ClassNotFoundException(name);
    }

img

Tomcat优化的思路

系统性能的衡量指标,主要是响应时间和吞吐量。

  • 响应时间:执⾏某个操作的耗时;

  • 吞吐量:系统在给定时间内能够⽀持的事务数量,单位为TPS(Transactions PerSecond的缩写,也就是事务数/秒,⼀个事务是指⼀个客户机向服务器发送请求然后服务器做出反应的过程。

Tomcat优化要从两个⽅⾯进⾏

  • JVM虚拟机优化(优化内存模型)

  • Tomcat⾃身配置的优化(⽐如是否使⽤了共享线程池?IO模型?)

JVM虚拟机优化主要是针对jvm参数和GC进行优化,对于GC的参数可以参考JVM中GC常用参数说明及理解GC日志

对于GC操作的内存可以参考Java内存区域(运行时数据区域)划分

JVM参数调整建议
参数作⽤优化建议
-server启动Server,以服务端模式运⾏ 服务端模式建议开启
-Xms最⼩堆内存建议与-Xmx设置相同
-Xmx最⼤堆内存建议设置为可⽤内存的80%
-XX:MetaspaceSize元空间初始值
-XX:MaxMetaspaceSize元空间最⼤内存默认⽆限
-XX:NewRatio年轻代和⽼年代⼤⼩⽐值,取值为整数,默认为2不需要修改
-XX:SurvivorRatioEden区与Survivor区⼤⼩的⽐值,取值为整数,默认为8不需要修改

在bin/catalina.sh文件中可以通过JAVA_OPTS调整JVM参数

参数调整示例:

JAVA_OPTS="-server -Xms2048m -Xmx2048m -XX:MetaspaceSize=256m -

XX:MaxMetaspaceSize=512m"

调整后可以通过 jhsdb jmap --heap --pid tomcat端口号 来查询tomcat中jvm配置

垃圾回收器参数建议
参数作⽤
-XX:+UseSerialGC启⽤串⾏收集器
-XX:+UseParallelGC启⽤并⾏垃圾收集器,配置了该选项,那么 -XX:+UseParallelOldGC默认启⽤
-XX:+UseParNewGC年轻代采⽤并⾏收集器,如果设置了 -XX:+UseConcMarkSweepGC选项,⾃动启⽤
-XX:ParallelGCThreads年轻代及⽼年代垃圾回收使⽤的线程数。默认值依赖于JVM使⽤的CPU个数
-XX:+UseConcMarkSweepGC(CMS)对于⽼年代,启⽤CMS垃圾收集器。 当并⾏收集器⽆法满⾜应⽤的延迟需求是,推荐使⽤CMS或G1收集器。启⽤该选项后, -XX:+UseParNewGC⾃动启⽤。
-XX:+UseG1GC启⽤G1收集器。 G1是服务器类型的收集器, ⽤于多核、⼤内存的机器。它在保持⾼吞吐量的情况下,⾼概率满⾜GC暂停时间的⽬标。

在bin/catalina.sh文件中可以通过JAVA_OPTS调整JVM参数

JAVA_OPTS="-XX:+UseConcMarkSweepGC"
Tomcat配置调优建议
  • 调整tomcat线程池

  • 调整tomcat的连接器

    调整tomcat/conf/server.xml 中关于链接器的配置可以提升应⽤服务器的性能。

    参数说明
    maxConnections最⼤连接数,当到达该值后,服务器接收但不会处理更多的请求, 额外的请求将会阻塞直到连接数低于maxConnections 。可通过ulimit -a 查看服务器限制。对于CPU要求更⾼(计算密集型)时,建议不要配置过⼤ ; 对于CPU要求不是特别⾼时,建议配置在2000左右(受服务器性能影响)。 当然这个需要服务器硬件的⽀持
    maxThreads最⼤线程数,需要根据服务器的硬件情况,进⾏⼀个合理的设置
    acceptCount最⼤排队等待数,当服务器接收的请求数量到达maxConnections ,此时Tomcat会将后⾯的请求,存放在任务队列中进⾏排序, acceptCount指的就是任务队列中排队等待的请求数 。 ⼀台Tomcat的最⼤的请求处理数量,是maxConnections+acceptCount
  • 禁⽤ A JP 连接器

  • 调整 IO 模式

    Tomcat8之前的版本默认使⽤BIO(阻塞式IO),对于每⼀个请求都要创建⼀个线程来处理,不适

    合⾼并发;Tomcat8以后的版本默认使⽤NIO模式(⾮阻塞式IO)

  • 动静分离

    可以使⽤Nginx+Tomcat相结合的部署⽅案,Nginx负责静态资源访问,Tomcat负责Jsp等动态资源访问处理(因为Tomcat不擅⻓处理静态资源)。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐