tomcat有两个主要功能:
其中负责客户端浏览器进⾏交互的叫连接器(connector)
负责处理业务逻辑的叫容器(container)
Coyote 是Tomcat 中连接器的组件名称 , 是对外的接⼝。客户端通过Coyote与服务器建⽴连接、发送请求并接受响应 。 其主要功能如下:
Tomcat Coyote ⽀持的协议与 IO模型如下:
协议:
HTTP/1.1 | 默认协议,大部分web采用的协议 |
---|---|
AJP | 用于和WX集成(比如Apache),实现了对静态资源的优化和集群部署,当前支持 AJP/1.3 |
HTTP/2 | HTTP 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 的影响性能。
组件 | 作⽤描述 |
---|---|
EndPoint | EndPoint 是 Coyote 通信端点,即通信监听的接⼝,是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint⽤来实现TCP/IP协议的 |
Processor | Processor 是Coyote 协议处理接⼝,如果说EndPoint是⽤来实现TCP/IP协议的,那么Processor⽤来实现HTTP协议,Processor接收来⾃EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应⽤层协议的抽象 |
ProtocolHandler | Coyote 协议接⼝, 通过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,再调⽤容器 |
Tomcat是⼀个由⼀系列可配置(conf/server.xml)的组件构成的Web容器,⽽Catalina是Tomcat的servlet容器。从另⼀个⻆度来说,Tomcat 本质上就是⼀款 Servlet 容器, 因为 Catalina 才是 Tomcat 的核⼼ , 其他模块都是为Catalina 提供⽀撑的。 ⽐如 : 通过 Coyote 模块提供链接通信,Jasper 模块提供 JSP 引擎,Naming 提供JNDI 服务,Juli 提供⽇志服务。
可以认为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请求,并返回对象给⽤户的模块
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的配置文件是conf/server.xml
<!--
Server 根元素,创建⼀个Server实例,⼦标签有 Listener、GlobalNamingResources、
Service
-->
<Server>
<!--定义监听器-->
<Listener/>
<!--定义服务器的全局JNDI资源 -->
<GlobalNamingResources/>
<!--
定义⼀个Service服务,⼀个Server标签可以有多个Service服务实例
-->
<Service/>
</Server>
...
...
Connector 标签⽤于创建链接器实例
默认情况下,server.xml 配置了两个链接器,⼀个⽀持HTTP协议,⼀个⽀持AJP协议
⼤多数情况下,我们并不需要新增链接器配置,只是根据需要对已有链接器进⾏优化
可以使⽤共享线程池:
Engine 表示 Servlet 引擎
...
Host 标签⽤于配置⼀个虚拟主机
...
Context 标签⽤于配置⼀个Web应⽤
当一个请求到达tomcat时,tomcat的mapper组件会把url和host、context、wrapper进行映射,比如http://localhost:8080/web_demo/resume/addResume,
host是负责映射localhost、context负责web_demo、wrapper负责resume/addResume 这样就可以找到对应的Servlet出来业务
请求处理流程如下:
Mapper组件体系结构:
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 ⽬录中的⽂件
当某个类被加载时,当前的类加载器会先把这个任务委托给父类加载器,递归这个操作,只有父类加载器找不到这类的时候,才会自己去加载这个类。
例如加载Test对象 AppClassLoader会先把这个任务交给ExtClassLoader,ExtClassLoader又会上交给BootstrapClassLoader,因为BootstrapClassLoader负责加载核心类库的类,ExtClassLoader负责加载扩展类,因此在它们负责的路径下是找不到这个类的,最后返回给AppClassLoader完成加载。
防止类被重复加载:向上委托的时候会判断该类是否已经被加载过了,如果父类加载器已经加载了,就不用再重复加载了.
防止核心Api被随意篡改:通过委托⽅式,不会去篡改核⼼.class,即使篡改也不会去加载,即使加载也不会是同⼀个.class对象了。不同的加载器加载同⼀个.class也不是同⼀个.class对象。这样保证了class执⾏安全(如果⼦类加载器先加载,那么我们可以写⼀些与java.lang包中基础类同名的类, 然后再定义⼀个⼦类加载器,这样整个应⽤使⽤的基础类就都变成我们⾃⼰定义的类了。)
类加载器的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;
}
}
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
可以看到在这里获取当前类加载器的父类加载器,如果不等于null则由父类加载器加载目标类,并且父类加载器也同样实现了该逻辑。
如果为null则调用findBootstrapClassOrNull方法。
那么什么时候或者什么样的类加载器,父类加载器才会为空呢?
扩展类加载器的上级加载器为null,这是因为启动类加载器不是由java编写的,所以在jvm中为null.
回到上面的源码,也就是当目标类委托到扩展类加载器之后继续向上委托时就会执行findBootstrapClassOrNull方法。
简单总结就是先找父亲加载,不行就给儿子加载。
Tomcat 的类加载机制相对于 Jvm 的类加载机制做了⼀些改变。没有严格的遵从双亲委派机制,也可以说打破了双亲委派机制,这主要是因为tomcat是一个web服务器需要支持部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
引导类加载器 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 默认改变了严格的双亲委派机制
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);
}
系统性能的衡量指标,主要是响应时间和吞吐量。
响应时间:执⾏某个操作的耗时;
吞吐量:系统在给定时间内能够⽀持的事务数量,单位为TPS(Transactions PerSecond的缩写,也就是事务数/秒,⼀个事务是指⼀个客户机向服务器发送请求然后服务器做出反应的过程。
Tomcat优化要从两个⽅⾯进⾏
JVM虚拟机优化(优化内存模型)
Tomcat⾃身配置的优化(⽐如是否使⽤了共享线程池?IO模型?)
JVM虚拟机优化主要是针对jvm参数和GC进行优化,对于GC的参数可以参考JVM中GC常用参数说明及理解GC日志
对于GC操作的内存可以参考Java内存区域(运行时数据区域)划分
参数 | 作⽤ | 优化建议 |
---|---|---|
-server | 启动Server,以服务端模式运⾏ 服务端模式 | 建议开启 |
-Xms | 最⼩堆内存 | 建议与-Xmx设置相同 |
-Xmx | 最⼤堆内存 | 建议设置为可⽤内存的80% |
-XX:MetaspaceSize | 元空间初始值 | |
-XX:MaxMetaspaceSize | 元空间最⼤内存 | 默认⽆限 |
-XX:NewRatio | 年轻代和⽼年代⼤⼩⽐值,取值为整数,默认为2 | 不需要修改 |
-XX:SurvivorRatio | Eden区与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/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不擅⻓处理静态资源)。