☰
Tomcat 8.5.16实战:解压即用、连接器调优与数据源配置全指南
2026/10/23 5:09:40 网站建设 项目流程

简介:Apache Tomcat v8.5.16 是 Java Web 开发中广泛使用的开源 Servlet 容器与 WEB 服务器发行包,面向后端开发人员、运维工程师及 Java Web 初学者,可快速搭建本地调试或生产部署环境,也可用于理解 Servlet/JSP 运行机制。压缩包约 9.53MB,共 637 个文件,涵盖 75 个 java 源文件、100 个 class 编译产物、57 个 jsp 动态页面、30 个 jar 依赖库,以及大量 html 文档、xml 配置、properties 参数、bat/sh 启停脚本、css 样式等类型,有助于开发者对照源码、配置与编译结果进行学习或排查问题。包内目录结构完整,包含启动、停止、配置校验等管理脚本,同时提供默认 Web 应用、示例页面与部署相关文件,解压后即可快速启动服务。已有 218 人学习下载,适合需要标准 Tomcat 环境、研究容器内部运行机制或搭建课程实验服务器的读者收藏使用。

1. 为什么是 8.5.16:一个解压即用的 Java Web 容器

我第一次遇到 Tomcat 8.5.16 这个版本,是在给一套老项目换服务器的时候。旧机器上跑的是 8.0 系,新机器只装了 JDK 8,项目里用的很多库都是 2016 年前后的,升级到 Tomcat 9 心里没底——倒不是不能用,而是新版本对 Servlet 规范的实现更激进,老代码里那些反射和资源路径的写法时不时就会翻车。当时同事给了一个 apache-tomcat-8.5.16 的压缩包,解压、配好 JAVA_HOME、启动,项目就起来了。后来我养成了一个习惯:手边常备一个 Tomcat 8.5.16 的 zip,遇到「只要一个能跑的 Java Web 容器」的场合,拆开就用,不用去翻系统包管理器里的旧版本,也不用担心 apt 源里那个 tomcat8 被改得面目全非。这篇笔记就把我拆这个包的完整过程写下来,从版本边界、目录职责、连接器参数,到 JVM 内存、数据源和排错清单,新手能照着启动一个服务,熟手可以直接拿参数表对照自己的环境。

2. 解压到启动:目录职责与两种启动姿势

2.1 版本的边界先立住:选 8.5.x 而不选 9.x 的关键

很多人上来就问 8.5 和 9.0 到底差在哪。从规范角度看,Tomcat 8.5 实现的是 Servlet 3.1、JSP 2.3,而 Tomcat 9 是 Servlet 4.0。Servlet 4.0 带来了 HTTP/2 的完整支持和更灵活的异步处理,但对大部分业务系统来说,这些特性在项目代码里根本用不到,反而升级后可能踩到一些坑:比如 9.x 对注解扫描的默认行为变了,某些类路径下扫描不到旧写法里的@WebServlet;又比如 9.x 默认把org.apache.jasper.compiler的一些编译选项收紧,老 JSP 里不规范的 Java 代码会直接编译报错。8.5.16 是 8.5 这条线上的中间版本,它的定位本来就介于 8.0 和 9.0 之间——既修掉了 8.0 里很多连接器和类加载的毛病,又没急着上 Servlet 4.0,兼容性风险最小。

我一般判断标准很简单:如果你的项目是用 JDK 7 或 JDK 8 编译的,代码里有老式web.xml配置,或依赖一些 2017 年之前发布的第三方库,8.5.x 比 9.x 稳得多。如果你是从零开始的新项目、确定只用 JDK 11+,那可以直接考虑 9.x 或更高版本。8.5.16 对 Java 7 之后到 Java 8 的环境支持很好,JDK 9 以后的模块化环境也能跑,但我不建议在 JDK 11 上跑 8.5 系做生产——虽然能启动,但某些反射相关的操作会频繁打 warning,性能不如直接用 9.x。这个版本最适合的场景就是:JDK 8 为主、老项目迁移、临时搭建测试环境。

还有一个很现实的理由:8.5.16 这个 zip 是绿色解压版,不写注册表、不依赖系统服务,拷贝到任何目录都能跑。Windows 上用startup.bat,Linux 上用startup.sh,前后端分离的项目里,后端服务挂在一个 Tomcat 上,前端 Nginx 转发过来,这套结构非常常见。

2.2 环境变量和目录职责:先分清 bin、conf、webapps

解压后你会得到一个apache-tomcat-8.5.16目录,里面六个关键子目录,先把职责分清,后面排错才不会到处乱翻。我做了个表:

目录作用常见误用
bin启动、停止、版本查询脚本有人把整个 bin 改成 777,没必要
confserver.xml、web.xml、context.xml 都在这里乱改后没有备份,改错了起不来
libTomcat 全局 jar 包业务用的 jar 别放这里,会污染所有应用
logscatalina.out、localhost.log 等日志日志目录不能删,否则启动会异常
webapps应用部署根目录,war 丢这里自动发布被塞入大量非 war 文件,扫描变慢
workJSP 编译后的 class 缓存更新 JSP 后不清理这里,页面没变化

启动前需要配两个环境变量:JAVA_HOME指向 JDK 根目录,CATALINA_HOME指向解压后的 Tomcat 根目录。如果不配CATALINA_HOME,脚本也可以根据自身位置推断,但手动配上是明确的做法。在 Linux 上我习惯把这两行写进/etc/profile:

export JAVA_HOME=/usr/local/jdk1.8.0_202 export CATALINA_HOME=/opt/tomcat/apache-tomcat-8.5.16 export PATH=$JAVA_HOME/bin:$PATH

这里有个关键点:JAVA_HOME一定要指到 JDK 的根目录,不能指到bin子目录。如果指错了,启动脚本里所有基于$JAVA_HOME/bin/java的调用都会失效。CATALINA_HOME我建议写完整路径,不要用~符号,否则某些版本的脚本在解析相对路径时会出错,这是我在某台 CentOS 机器上真实踩过的坑。

2.3 第一次启动:catalina.sh run 与 startup.sh 的差别

启动 Tomcat 有几种姿势,区别在于是不是在前台运行。新手最容易犯的错是直接双击startup.bat然后窗口一闪就没了,以为没装上——实际上是窗口关闭时把 Tomcat 进程也带关了。Linux 上我用得最多的是catalina.sh run,它会将日志直接打到当前终端:

cd /opt/tomcat/apache-tomcat-8.5.16 ./bin/catalina.sh run

这个命令适合首次启动排错。你会看到 INFO 级别的日志一行行出来,比如:

信息: Initializing ProtocolHandler ["http-nio-8080"] 信息: Starting ProtocolHandler ["http-nio-8080"] 信息: Server startup in [1,352] milliseconds

然后打开浏览器访问http://127.0.0.1:8080,能看到默认首页就说明基础环境没问题。注意这里说的是http-nio-8080,NIO 连接器是 8.5 以后默认启用的。确认没报错后,真正要放到后台跑时再用startup.sh:

./bin/startup.sh

它会启动一个独立的 JVM 进程并立刻返回。此时日志写到logs/catalina.out里,排错时看这个文件的尾部:

tail -f logs/catalina.out

启动相关的参数说明:bin/catalina.sh是核心脚本,run参数表示前台运行,start表示后台启动,stop用于停止;stop执行时会去找pid文件。如果启动失败,优先看catalina.out的最后 20 行,而不是看终端有没有输出——后台启动时终端只会显示一句Tomcat started。还有一个排查细节:8080 端口被占用时,startup.sh照样会打出 started,但catalina.out里有java.net.BindException: Address already in use,这就是为什么我每次都把看日志作为启动后第一件事。

3. 调优连接器:server.xml 里的端口、线程与超时关系

3.1 连接器参数:从连接超时到最大线程数

启动正常后,真正的工程问题才开始:连接数上不去、请求超时、偶尔卡死。这些问题八成出在conf/server.xml的 Connector 配置上。Tomcat 的默认配置是能跑,但远远没为高并发优化。我先给你看一套我常用的 Connector 配置,再逐个解释:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxThreads="200" minSpareThreads="20" maxConnections="10000" acceptCount="200" URIEncoding="UTF-8" />

这段配置的逻辑是:端口 8080 接收 HTTP 请求;connectionTimeout是建立连接后等待请求数据的最大毫秒数,20 秒合适,太短会导致弱网环境下的请求频繁断开,太长又容易被慢连接占住线程;maxThreads是处理请求的最大工作线程数,这个值不是越大越好,200 对应一般 4 核 8G 机器比较合理,过大会因为线程上下文切换导致吞吐量反而降低;minSpareThreads是空闲时保留的最小线程数,20 能避免突发流量时线程创建的开销;maxConnections是连接队列加正在处理连接的总上限,10 万是留给大量 Keep-Alive 连接的,如果内存吃紧,把它降到 5000 更稳妥;acceptCount是等待被处理的请求队列长度,相当于操作系统 accept 队列,200 够了,太大时你会在日志里看到大量请求排队超时。

改完server.xml后必须重启 Tomcat 生效,而且重启前建议先执行./bin/catalina.sh configtest检查 XML 语法。如果 XML 里多个元素配错,Tomcat 不会启动,只会给一句The server.xml configured to be invalid,这个时候 configtest 是救命的。

3.2 线程池 Executor 与 NIO 连接器的配合

默认情况下,每个 Connector 自己管理线程池,多个 Connector(比如 HTTP 和 HTTPS)各自为政,线程数不好统一控制。8.5 支持用<Executor>把线程池抽出来单独定义,再让 Connector 引用它。这样 HTTP 和 AJP 连接器可以共享同一个线程池,资源利用率更平均。

<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="300" minSpareThreads="20" maxQueueSize="100" />

配置后 Connector 里加上executor="tomcatThreadPool"属性即可,但要注意:一旦指定了 executor,Connector 上的maxThreads等线程相关属性会被忽略,很多人在这踩坑,以为改了两个地方取最大值,实际是 Executor 说了算。maxQueueSize是线程池内部队列的容量,超过队列后请求会被拒绝或等 acceptCount 处理,这个值设置过大会造成请求堆积,前端 Nginx 长时间不返回。一般场景 100 够用,除非你有批处理类的长任务。

NIO 连接器是 8.5 之后默认的,它和老的 BIO 模式最大区别是:线程不再一对一绑定到 Socket 连接,而是通过 Selector 复用,所以可以支撑更大数量的并发连接。但这有个后果——线程数变得不那么「看得见」,你ps -ef | grep java看到的线程数远低于实际连接数,这是正常现象,不是泄漏。

3.3 部署路径:appBase 与 docBase 的差事

把应用放进 Tomcat 有两种常见方式:直接丢 war 包到webapps,或者用 Context 指向外部目录。前一种适合快速部署,后一种适合应用与 Tomcat 分离管理。默认的appBase是webapps,Tomcat 启动时会扫描这个目录下的 war 包和目录,自动发布。unpackWARs默认 true,也就是说 war 会被解压成目录再运行。

外置部署时,在conf/Catalina/localhost下建一个 XML 文件,文件名就是应用路径。比如应用要跑在/demo,文件名为demo.xml:

<Context docBase="/data/www/demo" path="/demo" reloadable="false" crossContext="false" />

docBase可以是目录也可以是 war 文件路径,path是浏览器访问的上下文路径。这里有个容易搞混的点:XML 文件名已经决定了访问路径,path属性在这个写法下可以省略,如果你写了跟文件名不一致的path,内核会以文件名为准。reloadable我强烈建议生产环境设成 false,设成 true 虽然能热加载 class 变化,但会带来额外的类加载器开销,还可能引发内存泄漏,开发环境玩玩可以,生产环境开着这个等于给自己埋雷。

外部目录部署还有个好处:日志和配置文件可以和 Tomcat 本体完全隔离,升级 Tomcat 版本时不用迁移应用数据。我接手过一个老系统,应用里写死了绝对路径引用 Tomcat 的 webapps 位置,每次发布都要先停服务再切目录,后来我把 docBase 指到独立目录,发布就变成改软链接了,省了很多事。

4. 数据源与 JVM 内存:让 8.5.16 在生产里站稳

4.1 JNDI 数据源配置与 lib jar 的位置

Java Web 应用连数据库时,最规范的做法不是每个应用自己写DriverManager.getConnection,而是在 Tomcat 层配置 JNDI 数据源,应用通过jdbc/xxx名称取连接。Tomcat 8.5.16 里配置数据源的位置在conf/context.xml(全局)或者应用自己的META-INF/context.xml(应用独立)。我通常放在应用的META-INF/context.xml,这样应用挪到任何 Tomcat 上都能带过去。

<Context> <Resource name="jdbc/orderDB" auth="Container" type="javax.sql.DataSource" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://127.0.0.1:3306/order?useUnicode=true&amp;characterEncoding=UTF-8" username="root" password="123456" maxTotal="50" maxIdle="10" maxWaitMillis="10000" removeAbandonedOnBorrow="true" removeAbandonedTimeout="180" /> </Context>

配置逻辑不复杂,但你注意url里的&amp;,在 XML 里&字符必须转义成&amp;,否则 XML 解析直接失败。maxTotal是最大活动连接数,maxWaitMillis是请求连接的最大等待时间,超过这个时间会抛Cannot get a connection, pool exhausted。还有两个最容易被忽略但也最关键的参数:removeAbandonedOnBorrow和removeAbandonedTimeout。前者表示从连接池借用连接时检查是否有人放弃了连接,后者定义超时秒数。没有这两兄弟,连接池会越用越少,最终卡死在等待连接上——这是很多老系统跑几天就假死的经典原因。

数据源配好后,最典型的失败是ClassNotFoundException: com.mysql.jdbc.Driver。这是因为连接池要加载驱动类,而驱动 jar 必须放在 Tomcat 能加载到的地方——全局驱动放lib目录,只有某个应用用的驱动放应用的WEB-INF/lib。新人经常把 jar 丢在webapps根目录,Tomcat 根本不认,报错后翻半天日志也找不到原因。放对位置后还得注意 jar 版本,MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,跟 5.x 的com.mysql.jdbc.Driver不一样,配错了一个字母,同样报 ClassNotFound。

4.2 setenv.sh:定 JVM 内存和 GC 日志

Tomcat 的 JVM 内存默认值非常保守,默认堆最大只有物理内存的四分之一,对于生产应用来说完全不够用。调内存的正确位置不是catalina.sh而是建一个bin/setenv.sh(Linux)或bin/setenv.bat(Windows),因为catalina.sh会自动加载这个文件,直接改主脚本会把官方脚本文件搞脏,升级时又冲突。

export CATALINA_OPTS="-Xms1024m -Xmx2048m \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=512m \ -XX:+UseG1GC \ -XX:+HeapDumpOnOutOfMemoryError \ -XX:HeapDumpPath=/data/tomcat_dump \ -Djava.awt.headless=true"

参数含义:-Xms和-Xmx分别是堆初始值和最大值,我主张这两个设置成一样的值,避免 JVM 运行中动态伸缩堆带来的性能抖动,为了省事也为了防止堆收缩引发 Full GC 频繁。MetaspaceSize和MaxMetaspaceSize控制元空间(类元数据区),老项目用了大量动态代理或反射时,元空间容易暴涨,256/512M 是比较稳的组合。UseG1GC是 GC 策略,JDK 8 下 G1 适合堆大于 4G 的场景,如果你的堆只有 2G,用-XX:+UseParallelGC吞吐量反而更高。最后面的HeapDumpOnOutOfMemoryError是 OOM 时的后悔药,配好路径后如果哪天内存溢出,能留下一个.hprof文件,用jvisualvm打开就能看到是哪块内存撑爆的,没有这个参数,OOM 了就只能靠猜。

修改完成后重启时,验证参数是否生效:

ps -aux | grep java # 或者直接查进程启动参数 jinfo -flags <pid>

看到输出里出现-Xmx2048m就说明 setenv 生效了。注意CATALINA_OPTS和JAVA_OPTS是两个变量,前者只在 Tomcat 启动时用,后者会被全局 Java 进程继承,所以 JVM 内存配置放CATALINA_OPTS是对的,放JAVA_OPTS会把系统里其他 Java 程序的默认内存也改掉。

4.3 Manager 应用:解除默认锁定与再锁死

Tomcat 自带一个manager管理应用,能在线看应用列表、发布应用、查 JVM 状态,对排查问题很有用。但默认配置下 manager 是没有权限的——访问http://127.0.0.1:8080/manager/html会弹 403,因为conf/tomcat-users.xml里没有用户,或者用户没有对应角色。

给本地调试开权限时,编辑conf/tomcat-users.xml,在<tomcat-users>标签里加:

<role rolename="manager-gui"/> <user username="devadmin" password="onlyForDev" roles="manager-gui"/>

添加后重启 Tomcat,就能进入管理界面。但这里我要多提醒一句:这个账号必须立刻改掉,而且 manager-gui 角色只应该允许内网 IP 访问。8.5 的 manager 应用自带 IP 限制,默认允许 127.0.0.1,如果你要远程访问,得改webapps/manager/META-INF/context.xml里的allow属性。生产环境我最推荐的做法是:部署完应用后,直接把webapps/manager目录删掉,反正业务用不到,少一个暴露面。有人会觉得删了可惜,实际上catalina.out和 JMX 足够支撑绝大多数线上排查,manager 界面在高负载场景下打开还会卡顿,删除是合算的。

5. 避坑:端口被占、编码乱码、配置不生效的台账

5.1 现象:startup 提示成功但 8080 打不开

有一次我帮某开发者看一套环境,startup.sh执行后终端显示Tomcat started,但浏览器访问 8080 直接超时。我第一反应是看catalina.out,果然找到一行java.net.BindException: Address already in use: JVM_Bind。

原因是有另一个旧 Tomcat 实例早在 8080 端口上运行,新实例启动时端口抢占失败,但脚本没有把这次失败立即反馈到终端退出码上。解决方法是先确认占用者:Linux 用ss -lntp | grep 8080,Windows 用netstat -ano | findstr 8080,然后按 PID 查是什么进程。如果是残留的 java 进程,直接kill掉;如果被别的服务占用,就把server.xml里的 Connector 端口改成 8085 之类的不冲突端口。从那以后,我每次启动前都会习惯性地先查端口,而不是等启动完再试访问。

5.2 现象:URL 中文参数变成乱码或报 400

某个老项目在 Tomcat 8.0 上好好的,迁到 8.5.16 后,所有带中文 query 参数的请求都返回 400。看 Tomcat 日志,明确写着Invalid character found in the request target。

原因是我那台机器的server.xmlConnector 里没有显式设置URIEncoding="UTF-8"。Tomcat 8.0 之前某些版本默认按ISO-8859-1解析 URI,遇到中文字符就乱码;8.5 虽然默认变成 UTF-8,但如果你的 Connector 是从老配置文件拷过来的,旧属性会跟着覆盖。解决方式是在 Connector 上加URIEncoding="UTF-8",然后重启。这里要注意:URIEncoding只影响 URI 部分,不影响 POST 表单体的编码,表单乱码还得看应用自己的过滤器有没有设置request.setCharacterEncoding("UTF-8")。

5.3 现象:改了 server.xml 但参数没生效

某次我在一个测试环境调线程池,把maxThreads从默认的 200 改成 600,重启后jconsole一看线程数还是 200 左右,完全没有变化。

原因有两个可能:第一是改错了文件,conf/server.xml里其实有两个 Connector(8080 和 8009),有的人改的是 8009 那个 AJP 端口,8080 的 HTTP Connector 根本没动;第二是文件确实改了,但启动时 Tomcat 用了CATALINA_BASE指向的另一份配置。8.5 支持CATALINA_BASE与CATALINA_HOME分离,如果之前设置过CATALINA_BASE,它下面的conf/server.xml优先于CATALINA_HOME里的。解决方式是先./bin/catalina.sh configtest,然后grep maxThreads $CATALINA_BASE/conf/server.xml确认改的是实际加载的那份。我后来养成的习惯是:每次改配置后先catalina.sh version确认环境变量,再改文件,再 configtest。

5.4 现象:应用 JDBC 连接一直报 pool exhausted

跑了一天后突然大面积报错:Cannot get a connection, pool exhausted,重启后恢复,过一天又挂。

这个是最典型的连接池泄漏。业务代码里执行完数据库操作后没有finally { conn.close(); },连接池借出去的连接没有归还,200 次借出后池子空了。加上removeAbandonedOnBorrow="true"和removeAbandonedTimeout="180"后能自动回收,但本质上还是要修业务代码。排查时用show processlist;看 MySQL 连接数,Tomcat 侧用jstack <pid>抓线程,看有多少线程卡在数据源连接借用处。真正常见的方案是用连接池自带的监控,像 Tomcat JDBC Pool 的jmxEnabled=true可以走 JMX 查当前活动连接数。这个坑我至今见过不止十次,每次处理都是先开自动回收救急,再定位代码。

5.5 现象:更新了 JSP 但页面没变化

开发阶段改了一个.jsp文件,浏览器刷新还是旧页面,清缓存也没用。

原因一般是 Tomcat 的work目录缓存了编译后的 class。Tomcat 会按 JSP 文件时间戳决定是否重编译,但如果系统时间错乱,或者文件用scp上传来时保留了原时间戳,就会导致 Tomcat 认为 JSP 没变。解决方式:删除work/Catalina目录后重启。如果你有reloadable="true"的 Context,Tomcat 会定期扫描时间戳,但扫描周期是配置的,不是实时的。平时部署 JSP 相关的项目,我习惯在部署脚本里加一步rm -rf work/Catalina,省得这种时间戳问题变成玄学。

6. 重启后的验证动作:把「能跑」从感觉变成证据

每次配置改完、重启完成,光看到Tomcat started是不够的,那一行日志只代表监听端口起来了,不代表服务真的可用。我现在有一套固定的验证动作,全部做一遍大概两分钟,能挡住九成以上的低级故障。

第一步,先跑配置校验,检查 XML 有没有写崩:

cd /opt/tomcat/apache-tomcat-8.5.16 ./bin/catalina.sh configtest

输出OK再继续。这个命令不依赖 Tomcat 启动,它能单独解析所有配置文件,发现接口和 XML 语法问题。第二步,确认端口真实在监听:

ss -lntp | grep 8080

看到监听说明连接器起来了,但没确认能响应请求。第三步,用 curl 模拟真实 HTTP 请求:

curl -I http://127.0.0.1:8080/demo/health

-I是只拿响应头,如果返回 200 或 302,说明整个链路已经打通。如果返回 404,要分情况:/根访问 404 说明应用没部署上,/demo访问 404 说明 Context 路径配错了。如果返回 502,那不是 Tomcat 的问题,是前面 Nginx 或者防火墙没放行。第四步,看最近错误日志:

tail -n 50 logs/localhost.$(date +%Y-%m-%d).log

这个文件专门记应用部署和 Context 启动的报错,比 catalina.out 更直白。第五步,如果应用里用了数据库,我会直接 curl 一下应用里那个查询接口,确认不是能响应静态页但一碰数据库就挂。

这套验证流程的来源是有一次我碰到的现场:某系统升级 Tomcat 后,首页和静态资源都正常,但所有登录请求超时。当时系统管理员说「服务起来了」,但没人验证登录,直到用户反馈才发现连接池配置被新版本覆盖了。如果当时拿 curl 打一次登录接口,问题立刻就会暴露。从那以后,我每次换版本、改 server.xml、调 JVM 参数,都会强制走一遍 configtest、端口检查和接口探测,再放流量进来。希望这套检查习惯也能帮到你,少在半夜因为一个「以为没问题」的问题爬起来救火。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询