Tomcat 内部工作原理:你要的直白,而不是 PPT 里的花哨 装机的时候,Tomcat 是个低调的选手。它就是个标准的 Servlet 容器,啥也不干,只负责把 HTTP 请求给 Java 拿那会儿,Java 拿那会儿又拆成特定参数,最终通过 Response 往浏览器吐出一段 HTML。

这就够了,用户根本看不见它内部到底在折腾啥。但要是你盯着 Tomcat 的 log 看,要么想调优,你就得知道它到底是个啥鬼。

说白了,它就是 Java 语言的一个包装壳,把整个 Java 应用打包,交给 JVM 运行。

这个壳子上面贴满了各种标签:服务供给者、认证过滤器、线程池管理、AOP 切面,还有那个让你头疼的垃圾回收器。 它是如何建起来的? 实际上 Tomcat 的二进制文件里,最核心的就是那几份 Java 源码。你打开 Tomcat 的 jar 包,随意找个 class 文件,按名字打,比如 `catalina.jar` 要么 `catalina-core.jar`,就能蹦出来一堆 Java 方式。

这些方式有的写死了业务逻辑,有的只是调用了其他 jar 包里的类。Tomcat 的本质就是一个庞大的类加载器。 当你点击页面上的按钮,要么浏览器发来一个 GET 请求,Tomcat 的启动流程实际上就挺好办。

起初它读配置文件,比如 `conf/server.xml`。

这玩意儿指定了进程名、端口号,还有工作目录。程序启动后,JVM 会先入库,把 `catalina.jar` 里的各种类加载器注册进来。

这里有个关键点,Tomcat 赞成经典的“学校式”和“双亲委派”机制。

绝大多数请求都会告诉加载器:“这个包我不管,交给爸爸类加载器”。爸爸类加载器再去找,直到找到具体的业务逻辑。 这意味着 Tomcat 默认不会去加载 `post.jsp` 这种非标准路径下的资源,要不就配置特殊。

这也是为啥我们不用 `file:///` 直接做重定向,那样会触发浏览器直接加载文件,绕过 Tomcat 的转发机制。真正的转发是在 JVM 层面,利用 `Engine` 和 `CATALINA_REQUEST` 对象进行的。 你真正关心的:线程模型 大量人问 Tomcat 是不是 Java 的,实际上它更像是一个超高性能的 PHP 容器。

为啥?出于 PHP 也是用多线程写,但 PHP 的线程模型比较原始,主要是为了处理大量并发请求。Tomcat 则是为了解决高并发下的资源竞争难题,专门设计了基于线程池的模型。 想象一下你的网站瞬间爆满,没人能单核跑完。Tomcat 会创建一堆线程,每个线程负责处理一个请求。

这些线程不是无限生成的,而是一个个池子。请求来了,线程从池子里抽出来干活,干完就吐出来。

要是请求排队忒长,线程就被放回池子里等新的请求。 这个过程里有个细节,叫 `Thread Pool`。当 Servlet 被调用时,Tomcat 会在启动阶段就初始化一个 `Tomcat` 实例。

这个实例内部维护着一个线程池对象。

每次请求进来,Tomcat 先检查这个线程池是不是满了。

要是没满,直接复用现有的线程;要是满了,就新开一个线程。 这有个益处,也是缺点。用线程池的益处是线程复用,能大幅削减开销,出于线程对象的创建和销毁挺消耗 CPU。缺点就是争用资源。所有线程共享同一个内存空间,线程池里的线程要是都去访问同一个数据库,那数据库服务器就得时刻盯着这堆线程,协调它们。

要是数据库挂了,整个应用可能就得停机,出于没法卸载一个线程。 到底形成了啥? 实际上 Tomcat 最核心的局部就在 `Tomcat` 这个类和方式里。它拦截 HTTP 请求,检查 URL 是不是真的,后端地址是不是配置好的,然后调用后端 Servlet 接口。 Tomcat 有个特殊的设计,就是在启动 JVM 后,把整个项目加载成一个 `ApplicationContext`。

这个对象就像一个超级大的上下文管理器,里面挂满了所有的 Servlet、过滤器、监听器。当请求进来时,Tomcat 会遍历这个对象,找到对应的 Servlet 执行。 这里还有一个概念叫 `DispatcherServlet`。

这是 Tomcat 的核心管住器,它像一个总管家,接收请求,分发到具体的 Servlet 要么过滤器。

要是你没配置 `dispatcher`,Tomcat 会直接处理一个虚拟的 Servlet 路径,比如 `context-path` 下的某个类。 说到真正的业务逻辑,肯定离不开 `Servlet` 接口。Tomcat 内部有大量不同的 Servlet 实现,比如 `TomcatServlet`、`TomcatGenericServlet`。它们都继承了 `WebServlet` 接口。Tomcat 通过反射要么显式的注解,知道每个类对应哪个 URL 路径。 举个好办的例子,假设你在 `conf/` 目录下放了一个 `welcome` 类,Tomcat 的 `META-INF/context.xml` 文件里会写 `...`,然后 `...`。Tomcat 启动时,会把这个类加载到内存里,作为默认的欢迎页。当用户访问根路径时,Tomcat 就走这个类。 这里有个坑,`ContextLoaderListener` 的功能。

这个类监听目录下的文件变化。

比如你部署了 `src` 目录,Tomcat 会自动加载进去。但这有个难题,要是直接写 `webapp` 文件夹是绝对路径,Tomcat 每次重启都会重新加载整个文件,速度极快。但要是是 `src` 文件夹,Tomcat 每次启动都会去 `src` 里加载一次,别看快,但理论上只要文件变了,重启后就要重新加载所有代码。

这对性能要求高的场景来说,是个小瑕疵。 垃圾回收:最烧脑的局部 Tomcat 的 GC 算法特别复杂,出于它是双缓冲内存模型。Java 应用里有两类内存:`Heap` 和 `Metaspace`。Tomcat 为了性能,把 `Heap` 分成了好几块:`NonCaching`、`Caching`、`NewGen`、`OldGen` 等。 GC 的核心任务就是把满了的垃圾回收掉,腾出空间。Tomcat 用的是 G1 收集器。G1 和传统的 CMS 要么 ZGC 不一样,它能把内存切成大量块,用这些块去回收垃圾。

这种“切分”方式,让 G1 GC 在处理大对象时特别顺,不好办把大块内存一块块切得忒碎,影响性能。 但 GC 也分好坏。GC 本身是为了提升性能做的,但 JVM 启动时的 GC 往往挺耗时。

这是出于 JVM 刚启动时,内存还没定,对象还没分配好,状态还没初始化干净利落。

这时候 GC 就启动疯狂工作,害得启动慢。 Tomcat 在高并发场景下,要是线程忒多,G1 就会频繁起效,把内存切成挺细的碎片。

这会害得内存碎片化,最终把原本归于新垃圾的内存给回收了,结局就是 JVM 卡死。

这就是为啥 Tomcat 在高并发下需求动态调整 GC 的参数,比如调整 `MaxTenancy` 要么 `MaxRecycle` 的大小。 为啥不用 Nginx? 大量人问我,Tomcat 为啥不直接部署在 Nginx 后面? Nginx 的生命周期管理比 Tomcat 复杂得多。Nginx 是进程,每次请求都要加载模块、解析配置。

要是 Nginx 有 1000 个实例,那整个系统启动时就得执行 1000 次初始化。

这对于 Tomcat 来说,简直是灾难。Tomcat 启动一次,就能服务整个集群。 在 Nginx 后脸署 Tomcat,还有个主流方案是 `Proxy Pass`。Nginx 转发请求到 Tomcat,Tomcat 处理完后,把响应回给 Nginx。

这样 Nginx 只需求处理 HTTP 协议转义,负责负载均衡、SSL 加密。 但这有个致命难题:要是 Nginx 只负责转发,它如何知道 Tomcat 的日志在哪?要是 Nginx 打日志,Tomcat 就没法看;Tomcat 打日志,Nginx 也看不到。

这就是为啥大量架构师最终选择 Solr 要么别的搜索引擎做 Web 服务器,要么干脆拉倒 Nginx,直接用 Tomcat 做反向代理。 Tomcat 的优势就是好办粗暴。它不需求你写复杂的 Web 服务器代码,只要写一个 Servlet,就能跑起来。把 Nginx 作为反向代理一层,Tomcat 直接处理业务,这才是对的架构。 总结 Tomcat 就是个极致的 Java 容器。它设计之初就是为了高并发,用线程池模型把压力扛住,用 G1 GC 优化内存,用双缓冲模型避免 GC 卡顿。别看它内部藏着各种复杂的配置、监听器、类加载机制,就连涉及 AOP 切面、Spring 集成等,但在用户眼里,它就是一个默默运行的“壳”。 对于开发者来说,理解 Tomcat 的原理不是为了写代码,而是为了调优。

比如知道它的线程模型,就能合理设置线程数;知道它的 GC 特征,就能优化内存配置;知道它的启动流程,就能避免冷启动难题。

毕竟,Tomcat 如此了得,要是不理解它是如何跑的,你就没法让它跑得更快。 最终想说一句,Tomcat 不会告诉你,它为啥会这样设计。它只是告诉你:“只要按这个规矩来,请求就能正常转发。”这就是工具的本质。