JDK+Tomcat部署实战:从版本选型到问题排查全攻略

发布时间:2026/10/19 10:35:11
JDK+Tomcat部署实战:从版本选型到问题排查全攻略
搞后端开发的几乎没人敢说自己没碰过 JDK 和 Tomcat 这套组合。哪怕现在微服务、容器化喊得震天响Spring Boot 内嵌容器成了默认选择但大量存量系统、中小团队的项目、课程设计里的作业仍然跑在 JDK Tomcat 这套最经典的传统架构上。这篇文章就围绕后端开发里 JDK 和 Tomcat 的完整使用链路从版本选型、环境安装、项目部署、参数配置到常见问题排查把我这些年实际踩过的坑和验证过的方案一次性捋清楚。想系统掌握这套技术栈的可以当一份可抄作业的实操手册用正在被旧项目部署问题折磨的可以直接跳到问题排查部分找答案。无论你是刚入行的开发新人还是已经带项目的资深工程师这篇内容都值得花几分钟过一遍。1. 后端开发里的JDK与Tomcat这套组合到底在解决什么问题1.1 拆解JDK与Tomcat的分工逻辑要理解这套组合先把两个角色弄清楚。JDK 是 Java 开发工具包它不只是给你写代码用的编译器和运行环境还包含了 Java 类库、调试工具、监控工具等一大堆东西。后端 Java 程序最终能跑起来靠的是 JDK 里的 JVM。你把 .java 源码编译成 .class 字节码JVM 负责把字节码解释或编译成当前操作系统能识别的机器指令。换句话说JDK 是整个 Java 世界的底座你写的一切 Java 代码都跑在它之上。Tomcat 是一个 Servlet 容器同时也是轻量级的 Web 服务器。它的核心工作是接收 HTTP 请求把请求解析成 Java 层面的 HttpServletRequest 对象分发给你的 Servlet或者通过 Servlet 规范工作的 Spring MVC 框架代码然后把处理结果封装成 HttpServletResponse 响应返回给客户端。除此之外它还管理 Servlet 的生命周期、处理 JSP、支持 Session 管理、提供静态资源服务等。打个不太严谨但好理解的比方JDK 像发动机提供基础动力Tomcat 像变速箱和传动系统把发动机的动力转化成实际的行驶动作。没有发动机车根本动不了没有传动系统发动机再猛轮子也不会转。后端 Java 项目要对外提供服务这两个角色缺一不可。项目里所有业务逻辑、数据库访问、接口返回本质上都是在这套基础上运转的。1.2 为什么这套组合至今没有被淘汰现在新技术一轮接一轮很多人会问Spring Boot 都内嵌 Tomcat 了部署直接 java -jar 就行为什么还要单独装一个 Tomcat我拆过不少项目原因其实很朴素。第一传统企业项目的历史包袱。大批老项目是从 Servlet JSP 时代走过来的它们被打包成 WAR 包部署在独立的 Tomcat 容器里。项目没有重构动力跑得好好的谁也不会为了追新而瞎折腾。这种系统在银行、政企、传统软件公司里依然大量存在而且短期内看不到替换的可能。第二多应用隔离部署的需求。一台服务器上跑多个 Web 应用用独立的 Tomcat 实例做物理隔离或者在一个 Tomcat 里部署多个上下文路径的应用这种运维模型至今简单可靠。相比每个应用都起一个独立进程Tomcat 多应用部署在资源利用率和运维成本上都有优势。第三运维习惯和技能栈。很多运维团队对 Tomcat 的管理已经非常熟练监控、日志、重启流程全都有标准作业程序换成容器化方案需要搭一套新的基础设施成本并不低。一套稳定运行的系统在没有任何改造收益的前提下没有人愿意去动它。所以这篇文章聊的虽然是老技术但对相当一部分后端开发者来说它是日常工作里真正要面对的东西不是博物馆里的展品。2. 版本选型JDK 和 Tomcat 怎么搭配才能真正省心2.1 JDK 版本的真实差异JDK 从 8 到 21中间经历了无数个大版本迭代。但在后端开发的实际部署环境里我看到的分布大致是JDK 8 仍然占了很大的存量比例JDK 11 和 JDK 17 在新项目里逐渐成为主流JDK 21 开始有团队尝试但集中在基础设施较新的公司。这里有个核心概念必须搞清楚JDK 版本不是一个简单的数字越大越好它还绑定了 Java 语言规范、虚拟机实现、类库 API 的变化。JDK 8 引入了 Lambda 表达式和 Stream API让 Java 从啰嗦开始变得有点函数式。当年这是巨大的生产力提升很多老团队至今仍未完全迁移其上。JDK 9 开始引入模块化系统但实际使用率并不高后面版本默认还是类路径模式模块化带来的强封装收益在普通业务项目里并不明显。JDK 11 是长期支持版本提供了很多虚拟机层面的改进比如 G1 收集器成为默认、ZGC 开始出现但早期 ZGC 并不完善生产落地的团队也不多。JDK 17 也是长期支持版本性能、GC、安全都有不少打磨。最现实的好处是很多新框架、新工具链会默认要求 JDK 17 起步比如新版本的 Spring 框架。JDK 21 是最新的长期支持版本虚拟线程是一个大亮点对高并发 IO 密集型场景非常友好但生态适配还需要时间。我不建议在生产环境盲目追新除非你的团队有能力处理新版本带来的兼容性问题。后端开发的铁律永远是稳定优先。升级 JDK 带来的性能提升在大部分业务系统里感受并不明显但升级引发的问题往往是肉眼可见的。2.2 Tomcat 版本与 Servlet 规范之间的关系Tomcat 的版本选型有一个严格的对应关系Tomcat 版本对应特定的 Servlet 规范、JSP 规范和 Java EE 规范版本。我拿一张表直接展示选型时对照着看就行。Tomcat 版本Servlet 规范最低 JDK 要求典型适配场景Tomcat 8.5Servlet 3.1JDK 7JDK 8 老项目大量存量系统Tomcat 9.xServlet 4.0JDK 8传统 WAR 部署的主流选择Tomcat 10.xServlet 5.0JDK 8包名必须从 javax 换成 jakartaTomcat 11.xServlet 6.0JDK 17新项目JDK 17/21 的标配这里的包名从 javax 换成 jakarta是一个大坑。很多老项目从 Tomcat 9 升级到 10 的时候代码里 import 的是 javax.servlet.*到了 Tomcat 10 直接编译都过不了。因为 Java EE 移交给开源基金会后改名为 Jakarta EE命名空间整体变更。所以如果项目使用的是 Spring Boot 2.x建议继续用 Tomcat 9使用 Spring Boot 3.x 的项目天然就是 jakarta 命名空间选 Tomcat 10 以上版本没有障碍。2.3 我推荐的搭配组合基于实际项目经验我给几个直接的组合方案你直接照着抄问题不大。存量老项目JDK 8 Tomcat 8.5/9.x WAR 包部署。不要动版本不要听人忽悠升一下版本能提升多少性能收益不明显风险却不小。新传统项目仍用 WAR 部署JDK 17 Tomcat 10.1 或 11。JDK 17 是长期支持版本Tomcat 10.1 也很稳定Java 生态对新版本的适配已经比较成熟。微服务项目直接用 Spring Boot 内嵌 Tomcat。Spring Boot 3.x 自带 Tomcat 10.1这种情况下不需要单独部署 Tomcat但你仍然需要了解 Tomcat 的相关配置因为内嵌和独立部署的配置逻辑大同小异只是一个是自动装配一个是手动管理。教学和学习环境JDK 17 Tomcat 10.1。既避开老版本的一些历史问题又不会因为版本太新而找不到参考资料。记住一条底层逻辑Tomcat 的版本跟着 JDK 走项目的技术栈跟着 Spring Boot 版本走这三个因素要统一考虑。别 JDK 17 配个 Tomcat 8.5那是给自己挖坑到时候各种类库不兼容的报错能把你折腾到怀疑人生。3. 从零搭建JDK与Tomcat开发环境3.1 JDK 安装与环境变量配置的完整流程我以 Linux 环境为例说明假设你用的是常见的 CentOS 7.x 系列这是最常见的服务器环境。Windows 机器上做本地开发的逻辑完全一样只是路径写法不同。第一步下载 JDK。建议使用 OpenJDK 发行版因为商业 JDK 的许可模式比较复杂个人和企业使用容易踩到授权问题。网上有很多 OpenJDK 的镜像源选择你自己熟悉的一个就行。第二步解压安装。一般习惯把 JDK 放在 /usr/local/java 目录下。mkdir -p /usr/local/java tar -zxvf jdk-17_linux-x64_bin.tar.gz -C /usr/local/java/解压后目录名可能带有版本号例如 jdk-17.0.10。为了后续升级方便我习惯做一个软链接这样以后换新版本 JDK 只需要改链接指向不用改环境变量。ln -s /usr/local/java/jdk-17.0.10 /usr/local/java/jdk17第三步配置环境变量。编辑 /etc/profile 或者 ~/.bashrc 都可以一个生效于系统级一个生效于当前用户。export JAVA_HOME/usr/local/java/jdk17 export PATH$JAVA_HOME/bin:$PATH export CLASSPATH.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar这里多说一句JDK 9 之后已经没有单独的 dt.jar 和 tools.jar 了严格来说 CLASSPATH 可以不配。配不配都不影响日常使用把 JAVA_HOME 和 PATH 配好是最核心的。配置完执行source /etc/profile java -version看到类似 openjdk version 17.0.10 的输出说明环境基本没问题。第四步确认 javac 和 java 都属于同一个 JDK。有些机器上有系统自带的 OpenJDK在 /usr/bin/java 和 /usr/bin/javac 里先命中了旧版本。用 which java 和 which javac 检查一下路径如果有异常把 $JAVA_HOME/bin 放到 PATH 最前面确保优先使用你装的那个版本。3.2 Tomcat 的安装与启停操作Tomcat 的安装比 JDK 还简单下载一个 tar.gz 包解压到某个目录就能跑。我把解压后的目录习惯放在 /usr/local/tomcat这样路径简短写脚本也方便。tar -zxvf apache-tomcat-9.0.89.tar.gz -C /usr/local/ mv /usr/local/apache-tomcat-9.0.89 /usr/local/tomcat然后设置关键环境变量 CATALINA_HOMETomcat 启动脚本靠它找到自己所在的位置。export CATALINA_HOME/usr/local/tomcat启动、停止、重启的操作命令如下/usr/local/tomcat/bin/startup.sh /usr/local/tomcat/bin/shutdown.sh启动后别急着高兴先确认两件事一是 8080 端口有没有监听二是 Tomcat 的日志有没有报错。我用得最多的命令是netstat -tlnp | grep 8080 tail -f /usr/local/tomcat/logs/catalina.outcatalina.out 是 Tomcat 的主日志文件90% 的启动问题都能在里面找到线索。每次启停之后我建议都看一眼这个文件不要只看启动脚本的输出脚本输出只是告诉你脚本执行成功而已。3.3 配置防火墙与安全组时的注意细节本地环境可能一步到位服务器上部署就没那么简单了。这一步最大的坑是明明 Tomcat 起来了日志也正常但外网就是访问不了。原因十有八九是端口没放行。在 Linux 上如果用的是 firewalldfirewall-cmd --zonepublic --add-port8080/tcp --permanent firewall-cmd --reload如果用的是 iptables则需要单独加规则。如果你用的是某云服务商的服务器还要去云控制台的安全组里把 8080 端口加入白名单。安全组和机器防火墙是两个独立的层级任何一个挡着都访问不了。这里顺带提醒一个习惯别图省事把防火墙直接关了。我见过不少部署的人为了简单执行 systemctl stop firewalld结果服务器被扫描工具盯上各种爆破尝试不断。你的 Tomcat 如果暴露在公网至少不要使用默认的 8080 端口改成不常用的高位端口能避开大部分扫描程序的默认探测。生产环境更要养成最小暴露面的原则只开放必要的端口。4. 项目部署实操从 WAR 包到 Tomcat 的完整流程4.1 打包与部署的两种方式传统项目部署到 Tomcat无外乎两种方式。第一种也是用的最多的把打包好的 WAR 文件直接扔到 webapps 目录下。Tomcat 检测到新的 WAR 包后会自动解压部署。这是最经典的方式适合小项目、测试环境也适合没有自动化发布管线的团队。cp myapp.war /usr/local/tomcat/webapps/文件复制过去之后Tomcat 会自动解压出 myapp 目录等几秒访问 http://服务器IP:8080/myapp/ 就能看到应用首页。这种方式虽然简单但生产环境不建议直接这么干因为 Tomcat 的自动解压过程有时候会留下旧文件残留导致新代码没完全生效。第二种使用 Tomcat 的管理界面或 manager 脚本部署。通过 manager 的图形界面可以上传 WAR、启停应用、查看会话信息。但 manager 默认是关闭的需要编辑 conf/tomcat-users.xml 添加管理员账号。role rolenamemanager-gui/ user usernameadmin password一个足够强的密码 rolesmanager-gui/我极少在生产环境开 manager因为这是暴力破解的重点目标。如果你只是本地测试可以临时开一下生产环境一般用自动化方式部署比如通过 CI 工具的流程把 WAR 传到服务器、停 Tomcat、替换文件、再启动全程不需要开管理界面。4.2 上下文路径与虚拟主机配置访问地址里的那段路径术语叫上下文路径。默认情况下一个 myapp.war 包部署后上下文路径就是 /myapp。如果你想改成根路径 /有两种办法把 WAR 包改名为 ROOT.war 再放入 webapps在 conf/server.xml 的 Host 节点下增加配置Context path docBasemyapp reloadablefalse/我在实际项目中更喜欢用配置文件的方式明确、可控而且可以一次配多个应用不依赖文件名去控制路径。虚拟主机配置也在 server.xml 里。可以定义多个 Host让同一个 Tomcat 服务于多个域名。比如Host namewww.example-a.com appBase/srv/apps/a unpackWARstrue autoDeploytrue /Host Host namewww.example-b.com appBase/srv/apps/b unpackWARstrue autoDeploytrue /Host这样两个域名分别对应不同的应用目录互不干扰。我用过这个方案在一台 Tomcat 上同时运行三个不同客户的项目每个项目绑定不同域名前面再挂一层 Nginx 做反向代理整体非常干净客户各自访问自己的地址资源互相隔离。4.3 JVM 参数配置给 Tomcat 规划内存Tomcat 默认的 JVM 参数非常朴素生产环境必须调。最常用的配置文件是 bin/catalina.sh在文件开头找到合适位置加入自定义参数JAVA_OPTS-Xms512m -Xmx2048m -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m -XX:UseG1GC这几个参数一个个说清楚Xms 和 Xmx 是堆内存的初始值和最大值。建议两者设置成相同的值避免运行期堆扩容带来的性能抖动。堆扩容时 JVM 要执行内存复制在流量高峰期容易出现卡顿。MetaspaceSize 和 MaxMetaspaceSize 是元空间的初始值和最大值。JDK 8 之后方法元数据改存在元空间不再占用堆内存。元空间大小要按项目的类数量来估计老项目类多、代理类多512m 可能都不够用。UseG1GC 指定 G1 收集器。JDK 9 之后默认就是 G1但显式写出来更清晰也方便团队其他成员知道当前用的什么收集器。关于堆内存到底给多大给一个经验公式堆上限不要超过服务器物理内存的 50%。因为你不仅要给 Tomcat 里的 JVM 留内存操作系统本身、Nginx、数据库、日志缓冲等都要吃内存。如果一台 8G 内存的服务器只跑一个 TomcatXmx 给 3G 比较合理如果还跑着数据库Xmx 最多给 2G再多就容易把整机内存吃满触发系统 OOM。4.4 配置 Tomcat 的并发参数Tomcat 使用 Java 线程池处理请求核心配置在 conf/server.xml 的 Connector 节点上Connector port8080 protocolHTTP/1.1 connectionTimeout20000 maxThreads400 minSpareThreads20 acceptCount200 maxConnections10000 URIEncodingUTF-8/这些参数的含义maxThreads 是处理请求的最大线程数。设太高没用线程多了光是上下文切换就耗费大量 CPU还容易把内存吃满。通常 200 到 400 已经够用除非你的业务接口都很轻并且没有外部依赖阻塞否则盲目拉高线程数只会让系统更不稳定。minSpareThreads 是空闲时保留的最小线程数。它避免了流量突然涌进来时现场创建线程带来的延迟。这个值一般 10 到 20 就行。acceptCount 是等待队列的长度。当线程全忙时新的请求会进入系统的 Accept 队列排队。注意队列不等于并发处理请求在队列里等待响应时间会变长。maxConnections 是 Tomcat 同时接受的 TCP 连接数。连接数和线程数是两个概念连接可以排队等待线程处理。URIEncodingUTF-8 强制用 UTF-8 解析 URI解决 GET 请求的中文参数乱码问题。这些参数的值不是越大越好需要结合实际压测结果来调。我的习惯是先跑一轮压力测试看线程利用率、响应时间、错误率再决定往上调还是往下调。一上来就把 maxThreads 调到 1000 的十个里有九个会让系统更糟。5. 常见问题排查与避坑指南5.1 端口被占用Address already in use这个问题出现频率极高。常见场景之前的 Tomcat 进程没被杀干净或者你又启动了一个 Tomcat 实例。排查思路netstat -tlnp | grep 8080会看到一个 PID如果是 java 进程八成就是之前的 Tomcat 没停干净。处理方式kill -9 PID然后重新启动。如果 netstat 看不到占用但 Tomcat 仍然报端口被占用检查一下是不是端口被其他服务绑定或者是否叠加了端口转发规则这种情况比较少见但排查起来很费时间。这里说一个我踩过的坑在测试环境用 shutdown.sh 停 Tomcat 之后没检查进程就重新部署结果发现新代码没生效。后来才查到是旧的 Tomcat 进程还活着新启动实例因为端口冲突启动失败。所以后来我形成习惯每次启停之后先 netstat 确认端口监听状态再操作下一步。这个习惯看着简单确实能省掉不少冤案时间。5.2 内存溢出java.lang.OutOfMemoryError这是最影响线上稳定性的问题。常见的报错形式有三类Java heap space堆内存不够Metaspace元数据区溢出unable to create new native thread系统线程数达到上限堆内存不够是最常见的排查步骤一般是在 catalina.sh 里加上 GC 日志参数让 JVM 记录 GC 详情-XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/var/log/tomcat/gc.log用 jstat 查看 JVM 的堆使用情况jstat -gcutil PID 1000 10重点看 Full GC 的次数和耗时的趋势。如果 Full GC 频繁且回收后仍然有大量存活对象基本可以断定存在对象泄漏。用 jmap 导出堆快照jmap -dump:formatb,file/tmp/heap.hprof PID然后在分析工具里看大对象、看对象引用链定位到具体的类和方法。老实说堆转储分析对新手来说有点门槛但这是一项后端开发的必备技能值得花时间学会。不要一遇到内存溢出就只想着加大堆内存那只缓解症状不解决病灶。Metaspace 溢出多半是项目用了太多动态代理、CGLIB、反射生成的类。如果确认项目结构没问题适当调大 MaxMetaspaceSize 即可。5.3 类加载冲突ClassNotFoundException 与 NoSuchMethodErrorTomcat 的类加载机制是父类委托模式但是它的 Web 应用类加载器优先加载自己目录下的类。这个机制导致一个经典问题应用 lib 下放了某个库的旧版本同时容器 lib 下也有一份两个版本不一致运行时就会出现 NoSuchMethodError、NoClassDefFoundError 这类诡异问题。排查思路看启动日志中是否有类加载警告Tomcat 有时会输出自检提示。检查应用 WEB-INF/lib 和 Tomcat lib 目录下是否有重复的 jar 包。查看 class 的实际加载来源可以在启动参数加-XX:TraceClassLoading日志会输出每个类是从哪个 jar 加载的定位到具体的 jar 再对比版本。预防手段很简单也很重要Tomcat 的 lib 目录里尽量只放容器级别的公共库应用相关的库全部放到 WEB-INF/lib不要图省事往容器里塞。容器目录里的 jar 会被所有应用共享往里面放应用级依赖等于埋地雷哪天两个应用依赖同一个库的不同版本你就等着排查到半夜吧。5.4 部署后看不到新代码版本没更新这个问题排在部署事故的第一位。表象是你重新打包部署了浏览器上访问到的仍是旧功能。原因往往有三种Tomcat 没有解压新 WAR因为旧目录还在自动部署机制在某些配置下不会覆盖更新。浏览器或 Nginx 缓存了静态资源。部署过程没停 Tomcat虽然文件覆盖了但有些类没有真正重新加载。我的标准操作流程是用 shutdown.sh 正常停止 Tomcat。删除 webapps 下对应的旧应用目录和旧 WAR 包。复制新的 WAR 包到 webapps。删除 Tomcat 的 work 目录里面是 JSP 编译的中间产物留着容易出妖蛾子。启动 Tomcat再确认访问。这套流程每一步都有意义删除旧目录避免残留 class 干扰清空 work 目录避免旧 JSP 编译结果复活。在出现代码改了没生效的问题时按这个流程走一遍基本能解决。5.5 中文乱码问题乱码问题在 Tomcat 后端开发里几乎人人都会遇到。来源无非三个环节请求、响应、日志。请求乱码POST 请求要在字符编码过滤器里设置 UTF-8Spring MVC 有现成的 CharacterEncodingFilter 可以直接配置。GET 请求的 URI 编码则要在 Connector 上加 URIEncodingUTF-8。响应乱码给响应头加 Content-Type: text/html; charsetUTF-8或者代码里在写入之前设置 response.setCharacterEncoding(UTF-8)。日志乱码在 conf/logging.properties 里调整 UTF-8 相关配置并确保启动脚本里的 JVM 参数加上 -Dfile.encodingUTF-8。这三个环节全设置对乱码问题就能彻底消失。最怕的是只设置其中一两个当时看着好了过几天换个接口又冒出来那种零散排查最浪费时间。建议在一开始就把三个环节全部统一成 UTF-8这是最稳妥的做法。5.6 请求量一大就卡死线程耗尽某个时期连接数突增Tomcat 请求全部排队页面加载极慢CPU 却不高。这种情况基本是线程池被打满请求在 acceptCount 队列里排队等待。先别急着加线程数。要看线程卡在哪里。用 jstack 导出线程栈jstack PID thread_dump.txt分析线程栈如果发现大量线程卡在数据库连接获取上那问题在连接池配置而不是 Tomcat 线程池。如果卡在远程调用上要检查下游系统的性能。如果卡在锁竞争上要检查代码里有没有同步块过大的问题。给 Tomcat 加线程数是最后的手段不是第一手段。我有个项目就是这种情况当时运维直接把 maxThreads 调到 1000结果数据库连接池先爆了系统反而更不稳定。后来定位到问题是一次全表查询拖慢了所有请求加了个索引就解决了。线程数调得再高也救不了一个慢 SQL 拖垮的系统。6. 性能调优经验与日常维护心得6.1 静态资源分离让 Tomcat 专心处理动态请求Tomcat 的本质是 Servlet 容器处理静态文件并不擅长。但很多项目早期为了省事静态资源全部由 Tomcat 直接服务这在高并发下会浪费大量线程。常用的优化方案是用 Nginx 作为前置反向代理把静态资源请求直接交给 Nginx动态请求转发到 Tomcatserver { listen 80; location /static/ { alias /srv/static/; expires 7d; } location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这个组合方案的好处是Nginx 处理静态文件的性能远超 Tomcat还能做压缩、缓存控制。Tomcat 的资源全部留给动态请求处理。一套架构改下来在我实际维护的项目里高峰期的线程占用率能降 40% 以上。改动成本不高收益非常直接。6.2 连接器模式的选择NIO 是当前主流Tomcat 的连接器经历过 BIO、NIO、APR 三种主要模式。BIO 模式在 Tomcat 8.5 之后就不再建议使用了现在主流是 NIO。如果你在 server.xml 里看到 protocol 写成 HTTP/1.1那么默认用的就是 NIO 模式。APR 模式依赖操作系统级别的原生库在 Linux 上性能表现更好一点但部署复杂度明显高需要额外编译安装 native 库和 SSL 相关组件对服务器环境要求更苛刻。我在生产环境一般用 NIO 就够了除非压测数据显示 APR 有明确且稳定的优势否则不值得为它增加环境复杂度。毕竟对大多数业务系统来说真正的瓶颈在数据库查询、慢接口和外部依赖调用上而不在这层 IO 模型的选择上。6.3 定期查看 Tomcat 日志的排查习惯Tomcat 的日志体系分好几类别只盯着 catalina.out。还有两个很重要的localhost.logWeb 应用初始化、销毁、类加载相关日志应用启动报错经常在这里出现。localhost_access_log.txt访问日志记录所有请求的 URL、状态码、耗时。排查慢请求、异常请求时非常有用。我的习惯是写一个简单的脚本每天定时检查 access log 里的 5xx 状态码和响应时间超过 3 秒的请求汇总到工作群里。这个方法看起来原始但确实帮我在早期发现过几次潜在问题。有一次就是发现某个接口在每天下午三点准时变慢后来排查发现是定时任务的批量操作占了数据库连接影响了同步请求。如果等到用户反馈才去查早黄了。6.4 定时重启与优雅停机老项目还有个痛点长时间运行后即使没有明显内存泄漏JVM 也会因为堆碎片化、连接残留等导致性能下降。对于这种项目我见过很多团队直接使用凌晨低峰期自动重启的方案。重启不是最优解但确实能解决大量莫名其妙的性能问题。如果要用定时重启建议用 systemd 管理 Tomcat而不是裸跑 startup.sh。写一个简单的 systemd 服务文件[Unit] DescriptionTomcat Server Afternetwork.target [Service] Typeforking EnvironmentJAVA_HOME/usr/local/java/jdk17 EnvironmentCATALINA_HOME/usr/local/tomcat ExecStart/usr/local/tomcat/bin/startup.sh ExecStop/usr/local/tomcat/bin/shutdown.sh TimeoutStopSec60 Usertomcat Grouptomcat [Install] WantedBymulti-user.target这样就可以用 systemctl restart tomcat 做优雅重启。注意 ExecStop 用的是 shutdown.sh 而不是直接 kill因为 shutdown.sh 会走 JVM 的关闭钩子给应用一个清理资源和完成请求的机会。直接 kill 可能导致数据库事务中断、缓存数据不一致等问题在业务系统里不容小觑。在我实际维护这套 JDK Tomcat 技术栈的过程中最大的体会是这个组合之所以能活这么多年不是因为技术多么前沿而是因为它足够简单、足够稳定、出了问题有海量参考资料可查。新东西学得再多把 JDK 和 Tomcat 这套基本功打扎实处理线上问题的时候心里会特别有底。最后分享一个小习惯每次新装一台服务器跑 Tomcat我会写一个检查清单内容大致是端口通不通、日志有没有 ERROR、磁盘空间够不够、JVM 参数是不是生产配置。这四条看着不起眼但每一条都能在关键时刻救你一命。有一次就是靠检查磁盘空间提前发现了日志文件暴涨的问题避免了一次磁盘写满导致的服务宕机。