前言

在 Java 应用中,常用的 Web 服务器一般有 tomcat、weblogic、jetty、undertwo等。但从 Java 2019和2020 生态使用报告可以看到,tomcat的用户量对比明显较大,当然这也基于它开源和免费的特点。

Java 2019 年生态圈使用报告



Tomcat深入解析与性能优化_linux


2020 Java 生态系统报告



Tomcat深入解析与性能优化_大数据_02


从软件架构的发展角度来看,软件架构大致经历了如下几个阶段:



Tomcat深入解析与性能优化_linux_03


从当前企业使用的架构角度来说,使用SSM架构项目比较多,SSH基本被淘汰(大部分是老项目维护),很大一部分企业转向微服务架构了。

基于Spring 生态来说,大部分中小型企业都基本使用SpringBoot,SpringBoot本身集成了 tomcat、jetty和undertwo 容器,那么我们为什么需要花时间来研究tomcat呢?

  1. 当前tomcat依然是主流java web容器,研究它符合java 技术生态发展;
  2. 在java web项目调优中,如ssm项目中,在优化项目时,jvm和tomcat同样重要,都需要优化;
  3. 尽管springboot内置了tomcat容器,且配置了默认的tomcat参数,但当默认的tomcat参数满足不了项目优化要求时,就需要优化人员手动进行相关的参数优化,因此研究tomcat非常必要;
  4. 熟悉tomcat架构,是后续进行项目优化的基础,也是必备条件。

Tomcat架构说明

知识点:

  1. Tomcat目录结构
  2. Tomcat简要架构
  3. Tomcat各组件及关系
  4. Tomcat server.xml配置详解
  5. Tomcat启动参数说明(启动脚本)

「Tomcat」

是一个基于JAVA的WEB容器,其实现了JAVA EE中的 Servlet 与 jsp 规范,与Nginx Apache 服务器不同在于一般用于动态请求处理。在架构设计上采用面向组件的方式设计。即整体功能是通过组件的方式拼装完成。另外每个组件都可以被替换以保证灵活性。



Tomcat深入解析与性能优化_java_04


通过Tomcat官方可以看到,目前已经更新到Tomcat 10了,但当前大部分企业使用的Tomcat 为8或者9版本。



Tomcat深入解析与性能优化_tomcat_05


Tomcat 目录结构



Tomcat深入解析与性能优化_分布式_06


  • 「bin」:可执行文件,.sh结尾的表示linux可执行文件,.bat结尾的表示linux可执行文件
  • 「conf」:配置文件
  • 「lib」:tomcat相关jar包
  • 「temp」:临时文件
  • 「webapps」:存放项目
  • 「work」:工作目录

bin目录

bin目录存放可执行文件,简要结束常用命令



Tomcat深入解析与性能优化_tomcat_07


这里主要解释如下通用的命令,其他命令就不一一介绍

  • catalina.sh 真正启动Tomcat文件,可以在里面设置jvm参数
  • startup.sh 程序项目命令文件
  • version.sh 查看tomcat版本相关信息命令文件
  • shutdown.sh 关闭程序命令

conf目录

conf文件夹用来存放tomcat相关配置文件



Tomcat深入解析与性能优化_linux_08


「1.catalina.policy」

项目安全文件,用来防止欺骗代码或JSP执行带有像System.exit(0)这样的命令的可能影响容器的破坏性代码. 只有当Tomcat用-security命令行参数启动时这个文件才会被使用,即启动tomcat时, ​​startup.sh -security​​ 。



Tomcat深入解析与性能优化_分布式_09


上图中,tomcat容器下部署两个项目,项目1和项目2。由于项目1中有代码System.exit(0),当访问该代码时,该代码会导致整个tomcat停止,从而也导致项目2停止。

为了解决因项目1存在欺骗代码或不安全代码导致损害Tomcat容器,从而影响其他项目正常运行的问题,启动tomcat容器时,加上-security参数就,即​​startup.sh -security​​,如此即使项目1中有代码System.exit(0),也只会仅仅停止项目1,而不会影响Tomcat容器,然而起作用的配置文件就是catalina.policy文件。

「2.catalina.properties」

配置tomcat启动相关信息文件

「3.context.xml」

监视并加载资源文件,当监视的文件发生发生变化时,自动加载



Tomcat深入解析与性能优化_tomcat_10


「4.jaspic-providers.xml 和 jaspic-providers.xsd」

这两个文件不常用

「5.logging.properties」

该文件为tomcat日志文件,包括配置tomcat输出格式,日志级别等

「6.server.xml」

tomcat核心架构主件文件,下面会详细解析。

「7.tomcat-users.xml和tomcat-users.xsd」

tomcat用户文件,如配置远程登陆账号

tomcat-users.xsd 为tomcat-users.xml描述和约束文件

「8.web.xml」

tomcat全局配置文件。

lib目录

lib文件夹主要用来存放tomcat依赖jar包,如下为 tomcat 的lib文件夹下的相关jar包。



Tomcat深入解析与性能优化_tomcat_11


每个jar包功能,这里就不讲解了,这里主要分析ecj-4.13.jar,这个jar包起到将.java编译成.class字节码作用。

假设要编译MyTest.java,那么jdk会执行两步:

  • 第一步:将MyTest.java编译成MyTest.class

​javac MyTest.java​

  • 第二步:执行MyTest.class

​java MyTest.class​

  • 那么,使用ecj-4.13.jar如执行MyTest.java呢?

​java -jar ecj-4.13.jar MyTest.java​

logs目录

该文件夹表示tomcat日志文件,大致包括如下六类文件:



Tomcat深入解析与性能优化_java_12




Tomcat深入解析与性能优化_大数据_13


temp目录

temp目录用户存放tomcat在运行过程中产生的临时文件。(清空不会对tomcat运行带来影响)。

webapps目录

webapps目录用来存放应用程序,当tomcat启动时会去加载webapps目录下的应用程序。可以以文件夹、war包、jar包的形式发布应用。 当然,你也可以把应用程序放置在磁盘的任意位置,在配置文件中映射好就行。

work目录

work目录用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。 清空work目录,然后重启tomcat,可以达到清除缓存的作用。

Tomcat 简要架构



Tomcat深入解析与性能优化_tomcat_14


Tomcat 各组件及关系

  • Server 和 Service
  • Connector 连接器
  • HTTP 1.1
  • SSL https
  • AJP( Apache JServ Protocol) apache 私有协议,用于apache 反向代理Tomcat
  • Container
  • Engine 引擎 catalina
  • Host 虚拟机 基于域名 分发请求
  • Context 隔离各个WEB应用 每个Context的 ClassLoader都是独立
  • Component
  • Manager (管理器)
  • logger (日志管理)
  • loader (载入器)
  • pipeline (管道)
  • valve (管道中的阀)



Tomcat深入解析与性能优化_分布式_15


Tomcat server.xml 配置详解

Server 的基本基本配置:


<Server>
<Listener /><!-- 监听器 -->
<GlobaNamingResources> <!-- 全局资源 -->
</GlobaNamingResources
<Service> <!-- 服务 用于 绑定 连接器与 Engine -->
<Connector 8080/> <!-- 连接器-->
<Connector 8010 /> <!-- 连接器-->
<Connector 8030/> <!-- 连接器-->

<Engine> <!-- 执行引擎-->
<Logger />
<Realm />
<host "www.test.com" appBase=""> <!-- 虚拟主机-->
<Logger /> <!-- 日志配置-->
<Context "/applction" path=""/> <!-- 上下文配置-->
</host>
</Engine>
</Service>
</Server>


server

root元素:server 的顶级配置

主要属性: port:执行关闭命令的端口号 shutdown:关闭命令

  • [ ] 演示shutdown的用法
#基于telent 执行SHUTDOWN 命令即可关闭
telent 127.0.0.1 8005
SHUTDOWN


service

服务:将多个connector 与一个Engine组合成一个服务,可以配置多个服务。

Connector

连接器:用于接收 指定协议下的连接 并指定给唯一的Engine 进行处理。

主要属性:


protocol     监听的协议,默认是http/1.1
port 指定服务器端要创建的端口号
minThread 服务器启动时创建的处理请求的线程数
maxThread 最大可以创建的处理请求的线程数
enableLookups 如果为true,则可以通过调用request.getRemoteHost()进行DNS查询来得到远程客户端的实际主机名,若为false则不进行DNS查询,而是返回其ip地址
redirectPort 指定服务器正在处理http请求时收到了一个SSL传输请求后重定向的端口号
acceptCount 指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理,默认100;
address 绑定客户端特定地址,127.0.0.1
bufferSize 每个请求的缓冲区大小 bufferSize * maxThreads
compression 是否启用文档压缩
compressionMinSize 文档压缩的最小大小
compressableMimeTypes text/html,text/xml,text/plain
connectionTimeout 客户端发起链接到服务端接收为止,指定超时的时间数(以毫秒为单位)
connectionUploadTimeout upload情况下连接超时时间
disableUploadTimeout 如果为true则使用 connectionTimeout
keepAliveTimeout 当长链接闲置 指定时间主动关闭 链接 ,前提是客户端请求头 带上这个 head"connection" " keep-alive"
maxKeepAliveRequests 最大的 长连接数 默认最大100
maxSpareThreads BIO 模式下 最多线闲置线程数
minSpareThreads BIO 模式下 最小线闲置线程数
SSLEnabled 是否开启 sll 验证,在Https 访问时需要开启。


演示配置多个Connector


<Connector port="8860" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8862"
URIEncoding="UTF-8"
useBodyEncodingForURI="true"
compression="on" compressionMinSize="2048"
compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x-json,application/json,application/x-javascript"
maxThreads="1024" minSpareThreads="200"
acceptCount="800"
enableLookups="false"
/>


Engine

引擎:用于处理连接的执行器,默认的引擎是catalina。一个service 中只能配置一个Engine。

主要属性:name 引擎名称 defaultHost 默认host

Host

虚拟机:基于域名匹配至指定虚拟机。类似于nginx 当中的server,默认的虚拟机是localhost.

演示配置多个Host


<Host name="www.test.com"  appBase="/usr/www/test"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="www.luban.com.access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>


Context

应用上下文:一个host 下可以配置多个Context ,每个Context 都有其独立的classPath。相互隔离,以免造成ClassPath 冲突。

演示配置多个Context


<Context docBase="hello" path="/h" reloadable="true"/>


Valve

阀门:可以理解成request 的过滤器,具体配置要基于具体的Valve 接口的子类。以下即为一个访问日志的Valve.


<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="www.luban.com.access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />


Tomcat启动参数说明

我们平时启动Tomcat过程是怎么样的?

  1. 复制WAR包至Tomcat webapp 目录。
  2. 执行starut.bat 脚本启动。
  3. 启动过程中war 包会被自动解压装载。

但是我们在Eclipse 或idea 中启动WEB项目的时候 也是把War包复杂至webapps 目录解压吗?显然不是,其真正做法是在Tomcat程序文件之外创建了一个部署目录,在一般生产环境中也是这么做的 即:Tomcat 程序目录和部署目录分开 。我们只需要在启动时指定CATALINA_HOME 与 CATALINA_BASE 参数即可实现。



Tomcat深入解析与性能优化_分布式_16


可以编写一个脚本 来实现自定义配置:

「更新 启动 脚本」


#!/bin/bash 
export JAVA_OPTS="-Xms100m -Xmx200m"
export JAVA_HOME=/root/svr/jdk/
export CATALINA_HOME=/root/svr/apache-tomcat-7.0.81
export CATALINA_BASE="`pwd`"

case $1 in
start)
$CATALINA_HOME/bin/catalina.sh start
echo start success!!
;;
stop)
$CATALINA_HOME/bin/catalina.sh stop
echo stop success!!
;;
restart)
$CATALINA_HOME/bin/catalina.sh stop
echo stop success!!
sleep 3
$CATALINA_HOME/bin/catalina.sh start
echo start success!!
;;
version)
$CATALINA_HOME/bin/catalina.sh version
;;
configtest)
$CATALINA_HOME/bin/catalina.sh configtest
;;
esac
exit 0


「自动部署脚本」


#!/bin/bash -e
export now_time=$(date +%Y-%m-%d_%H-%M-%S)
echo "deploy time:$now_time"

app=$1
version=$2
mkdir -p war/
#从svn下载程序至 war目录
war=war/${app}_${version}.war
echo "$war"
svn export svn://192.168.0.253/release/${app}_${version}.war $war

deploy_war() {
#解压版本至当前目录
target_dir=war/${app}_${version}_${now_time}
unzip -q $war -d $target_dir
rm -f appwar
ln -sf $target_dir appwar
target_ln=`pwd`/appwar
echo '<?xml version="1.0" encoding="UTF-8" ?>
<Context docBase="'$target_ln'" allowLinking="false">
</Context>' > conf/Catalina/localhost/ROOT.xml
#重启Tomcat服务
./tomcat.sh restart
}

deploy_war


Tomcat 网络通信模型剖析

Tomcat 支持四种线程模型介绍

什么是IO?

IO是指为数据传输所提供的输入输出流,其输入输出对象可以是:文件、网络服务、内存等。



Tomcat深入解析与性能优化_java_17


什么是IO模型?

提问:假设应用在从硬盘中读取一个大文件过程中,此时CPU会与硬盘一样出于高负荷状态么?

演示:

演示观察大文件的读写过程当中CPU 有没有发生大波动。

演示结果:

CPU 没有太高的增涨

通常情况下IO操作是比较耗时的,所以为了高效的使用硬件,应用程序可以用一个专门线程进行IO操作,而另外一个线程则利用CPU的空闲去做其它计算。这种为提高应用执行效率而采用的IO操作方法即为IO模型。

各IO模型简要说明



Tomcat深入解析与性能优化_linux_18


使用指定IO模型的配置方式:

配置 server.xml 文件当中的 修改即可。

默认配置 8.0 protocol=“HTTP/1.1” 8.0 之前是 BIO, 8.0 之后是 NIO

  • 「BIO」

protocol=“org.apache.coyote.http11.Http11Protocol”

  • 「NIO」

protocol=“org.apache.coyote.http11.Http11NioProtocol”

  • 「AIO」

protocol=“org.apache.coyote.http11.Http11Nio2Protocol”

  • 「APR」

protocol=“org.apache.coyote.http11.Http11AprProtocol”

Tomcat BIO、NIO实现过程源码解析

BIO 与NIO区别

「分别演示在高并发场景下BIO与NIO的线程数的变化?」



Tomcat深入解析与性能优化_linux_19


「BIO 配置」


<Connector port="8080" protocol="org.apache.coyote.http11.Http11Protocol"
connectionTimeout="20000"
redirectPort="8443"
compression="on" compressionMinSize="1024"
compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x-json,application/json,application/x-javascript"
maxThreads="500" minSpareThreads="1"/>


「NIO配置」


<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol"
connectionTimeout="20000"
redirectPort="8443"
compression="on" compressionMinSize="1024"
compressableMimeType="text/html,text/xml,text/plain,text/javascript,text/css,application/x-json,application/json,application/x-javascript"
maxThreads="500" minSpareThreads="1"/>


演示数据:



Tomcat深入解析与性能优化_java_20


生成环境重要因素:

  1. 网络
  2. 程序执行业务用时

源代码地址:​​https://github.com/org-hejianhui/bit-bigdata-transmission​

「BIO 线程模型」



Tomcat深入解析与性能优化_分布式_21


「BIO 源码」

线程组:

Accept 线程组 acceptorThreadCount 默认1个

exec 线程组 maxThread

JIoEndpoint

Acceptor extends Runnable

SocketProcessor extends Runnable



Tomcat深入解析与性能优化_分布式_22


「NIO 线程模型」



Tomcat深入解析与性能优化_java_23


「NIO 线程模型」

Accept 线程组 默认两个轮询器

Poller Selector PollerEvent轮询线程状态

SocketProcessor



Tomcat深入解析与性能优化_大数据_24


「BIO」

线程数量 会受到 客户端阻塞、网络延迟、业务处理慢===>线程数会更多。

「NIO」

线程数量 会受到业务处理慢===>线程数会更多。

Tomcat connector 并发参数解读



Tomcat深入解析与性能优化_分布式_25


Tomcat 类加载机制源码解析

类加载的本质

是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 _.class,也可以是 jar 包里的 _.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

JVM 运行实例中会存在多个 ClassLoader,不同的 ClassLoader 会从不同的地方加载字节码文件。它可以从不同的文件目录加载,也可以从不同的 jar 文件中加载,也可以从网络上不同的静态文件服务器来下载字节码再加载。

jvm里ClassLoader的层次结构



Tomcat深入解析与性能优化_分布式_26


类加载器层次结构

「BootstrapClassLoader(启动类加载器)」

称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等,可通过如下程序获得该类加载器从哪些地方加载了相关的jar或class文件:


URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for (URL url : urLs) {
System.out.println(url.toExternalForm());
}


程序执行结果如下:


file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/classes


从rt.jar中选择String类,看一下String类的类加载器是什么


ClassLoader classLoader = String.class.getClassLoader();
System.out.println(classLoader);


执行结果如下:


null


可知由于BootstrapClassLoader对Java不可见,所以返回了null,我们也可以通过某一个类的加载器是否为null来作为判断该类是不是使用BootstrapClassLoader进行加载的依据。

「ExtensionClassLoader」

ExtClassLoader称为扩展类加载器,主要负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目录下的所有jar包或者由java.ext.dirs系统属性指定的jar包.放入这个目录下的jar包对AppClassLoader加载器都是可见的(因为ExtClassLoader是AppClassLoader的父加载器,并且Java类加载器采用了委托机制)。

ExtClassLoader的类扫描路径通过执行下面代码来看一下:


String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")) {
System.out.println(path);
}


执行结果如下(Mac系统):


/Users/hjh/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/jre/lib/ext:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java


jre/lib/ext路径下内容为:



Tomcat深入解析与性能优化_分布式_27


从上面的路径中随意选择一个类,来看看他的类加载器是什么:


sun.misc.Launcher$ExtClassLoader@4439f31e
null


从上面的程序运行结果可知ExtClassLoader的父加载器为null,之前说过BootstrapClassLoader对Java不可见,所以返回了null。ExtClassLoader的父加载器返回的是null,那是否说明ExtClassLoader的父加载器是BootstrapClassLoader?

❝Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。然后呢,我们前面已经分析了,JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作用一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象

「AppClassLoader」

才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的。

加载System.getProperty("java.class.path")所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如:java -cp ./lavasoft/classes HelloWorld


public class AppClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoader.getSystemClassLoader());
}
}


输出结果如下:


sun.misc.Launcher$AppClassLoader@18b4aac2


以上结论说明调用​​ClassLoader.getSystemClassLoader()​​可以获得AppClassLoader类加载器。


protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}


通过查看ClassLoader的源码发现并且在没有特定说明的情况下,用户自定义的任何类加载器都将该类加载器作为自定义类加载器的父加载器。

通过执行上面的代码即可获得classpath的加载路径。

在上面的main函数的类的加载就是使用AppClassLoader加载器进行加载的,可以通过执行下面的代码得出这个结论


public class AppClassLoaderTest {

public static void main(String[] args) {
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
}

private static class Test {

}

}


执行结果如下:


sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@2d209079


从上面的运行结果可以得知AppClassLoader的父加载器是ExtClassLoader

「Tomcat的 类加载顺序」



Tomcat深入解析与性能优化_tomcat_28


在Tomcat中,默认的行为是先尝试在Bootstrap和Extension中进行类型加载,如果加载不到则在Webapp ClassLoader中进行加载,如果还是找不到则在Common中进行查找。

NoClassDefFoundError

NoClassDefFoundError是在开发JavaEE程序中常见的一种问题。该问题会随着你所使用的JavaEE中间件环境的复杂度以及应用本身的体量变得更加复杂,尤其是现在的JavaEE服务器具有大量的类加载器。在JavaDoc中对NoClassDefFoundError的产生是由于JVM或者类加载器实例尝试加载类型的定义,但是该定义却没有找到,影响了执行路径。换句话说,在编译时这个类是能够被找到的,但是在执行时却没有找到。这一刻IDE是没有出错提醒的,但是在运行时却出现了错误。

NoSuchMethodError

在另一个场景中,我们可能遇到了另一个错误,也就是NoSuchMethodError。NoSuchMethodError代表这个类型确实存在,但是一个不正确的版本被加载了。

ClassCastException

ClassCastException,在一个类加载器的情况下,一般出现这种错误都会是在转型操作时,比如:A a = (A) method();,很容易判断出来method()方法返回的类型不是类型A,但是在 JavaEE 多个类加载器的环境下就会出现一些难以定位的情况。


原作者:一角钱小助手


Tomcat深入解析与性能优化_大数据_29